【问题标题】:Weird HashSet contains() behaviour奇怪的 HashSet contains() 行为
【发布时间】:2012-03-16 20:02:51
【问题描述】:

Java 中的 HashSet 让我很困惑,使用 contains() 时会查找 hashcode() 和 equals() 结果吗? 但在这种情况下,它的行为不正常。 如果将这种代码放在大型项目中,有时会出现问题。 问题是为什么最后一条语句 print FALSE ? contains() 到底是做什么的?

class R
{
    int count;
    public R(int count)
    {
        this.count = count;
    }
    public String toString()
    {
        return "R(count attribution:" + count + ")";
    }
    public boolean equals(Object obj)
    {
        if (obj instanceof R)
        {
            R r = (R)obj;
            if (r.count == this.count)
            {
                return true;
            }
        }
        return false;
    }
    public int hashCode()
    {
        return this.count;
    }
}
public class TestHashSet2
{
    public static void main(String[] args) 
    {
        HashSet hs = new HashSet();
        hs.add(new R(5));
        hs.add(new R(-3));
        hs.add(new R(9));
        hs.add(new R(-2));

        System.out.println(hs);

        //change first element
        Iterator it = hs.iterator();
        R first = (R)it.next();
        first.count = -3;
        System.out.println(hs);
        //remove
        hs.remove(new R(-3));
        System.out.println(hs);

        R r1 = new R(-3);
        System.out.println(r1.hashCode());
        Iterator i = hs.iterator();
        R r2 = (R)i.next();
        System.out.println(r2.hashCode());   //same hashcode -3
        System.out.println(r1.equals(r2));   //equals true

        System.out.println("hs contains object which count=-3 ?" + hs.contains(new R(-3)));  //false
    }
}

【问题讨论】:

标签: java contains


【解决方案1】:

问题在于R 对象的哈希码可以更改。这违反了hashCode() 方法应该遵守的约定。


要了解为什么这很重要,您需要了解哈希表的工作原理。 Java HashSet 的核心是一个条目列表数组。当你将一个对象放入哈希表时,它首先计算对象的哈希码。然后它通过计算将哈希码减少到数组中的一个索引

index = hashcode % array.length

然后它搜索从array[index] 开始的链,如果该对象不在列表中,则添加它。

为了测试 HashSet 是否包含对象,它执行相同的计算并搜索相同的哈希链。

但是,如果您对对象执行某些操作以使其哈希码在其位于表中时发生更改,则上述算法将(通常)在与最初添加到的链不同的链中查找对象。当然它不会找到它。

最终结果是,如果任何对象的哈希码合同被破坏,而该对象是集合的成员,则 HashSet 将表现异常。


这是 Java 7 javadoc 所说的(参见 java.jang.Object#hashcode() ):

"hashCode的一般合约是:

  • 只要在 Java 应用程序的执行过程中多次在同一个对象上调用它,hashCode 方法必须始终返回相同的整数,前提是没有修改对象上的 equals 比较中使用的信息。这个整数不需要从一个应用程序的一次执行到同一应用程序的另一次执行保持一致。

  • ...

“没有提供任何信息......” 警告让我感到困惑。我认为只有当对象哈希码在哈希表中时,还有一条规则不会导致对象哈希码发生变化时,它才有效。不幸的是,在您希望找到它的任何地方都没有说明此规则。文档错误?


也许我们应该将不更改哈希码的要求称为“口头合同”? :-)

【讨论】:

  • 其实并没有违反hashCode()的约定(只需要和equals()保持一致),HashSet或者HashMap的API doc好像没有提到这个要么。
【解决方案2】:

通过在将对象插入HashSet 后更改对象的值,您正在破坏数据结构的完整性。在那之后,你就不能依赖它来完成它的工作了。

使用可变对象作为任何映射的键或集合的值通常是一个坏主意。幸运的是,最常用于此目的的类(StringInteger)是不可变的。

【讨论】:

    【解决方案3】:

    这正是您不应该在 HashSet 和 HashMap 中使用可变对象作为键的原因。

    第一个迭代器返回哈希码为 5 的 R 对象。然后您更改了该对象的属性(计数)。但这并不强制重新计算哈希。因此,就 HashSet 而言,您将计数更改为 -3 的对象仍然位于哈希码 5 对应的存储桶中。然后您将位于哈希码 -3 对应的存储桶中的对象移除,这是原始的 R(-3) 对象。所以在该操作之后,桶中没有哈希码为-3的对象,所以contains(new R(-3))返回false。

    【讨论】:

    • 看看HashMap的实现是这样的
    【解决方案4】:

    HashSet 将值存储在 buckets 中,当您将元素添加到哈希集中时会计算存储桶索引。其背后的想法:现在该集合可以读取对象哈希码并一步计算存储桶。换句话说:contains() 是一个 O(1) 操作。

    想象一个简单的哈希集:

    bucket    object(hashcode)
    #1        5
    #2        -3
    #3        6
    

    使用哈希函数来计算桶,例如:

    f(hashcode) :=  |  5 -> 1
                    | -3 -> 2
                    |  6 -> 3
    

    现在看看您在示例中做了什么:您删除了存储桶 2 中的对象(更改了函数)并更改了存储桶 1 中对象的哈希码。

    新功能如下:

    f(hashcode) :=  |  5 -> 1
                    |  6 -> 3
    

    f(-3) 将返回 null(contains() 返回 false),并且您的哈希码为 -3 的实际对象存储在哈希码为 5 的对象应该在的位置。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-08-27
      • 2011-07-27
      • 1970-01-01
      • 2019-04-15
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多