奇怪的HashSet包含()行为

时间:2012-02-27 10:30:37

标签: java contains

java中的HashSet让我很困惑,当使用contains()时会查找hashcode()和equals()结果吗? 但在这种情况下,它表现不正常。 如果你把这种代码放在大型项目中,有时会出现问题。 问题是为什么最后一个语句打印FALSE?什么包含()在引擎盖下做什么?

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
    }
}

4 个答案:

答案 0 :(得分:6)

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

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

答案 1 :(得分:2)

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

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

答案 2 :(得分:2)

HashSet将值存储在存储区中,当您将元素添加到哈希集时,会计算存储区索引。它背后的想法:现在该集合可以读取对象哈希码并在一个步骤中计算存储桶。换句话说: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的对象应该是。

答案 3 :(得分:1)

问题是R对象的哈希码可以更改。这违反了hashCode()方法应遵守的合同。


要理解为什么这很重要,您需要了解哈希表的工作原理。 Java HashSet的核心是一系列条目列表。将对象放入哈希表时,它首先计算对象的哈希码。然后通过计算

将哈希码减少到数组中的索引
index = hashcode % array.length

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

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

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

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


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

  

“hashCode的一般合约是:

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

  •   
  • ...

  •   

“没有提供任何信息......”告诫我。我认为它只有在有一个规则不会导致对象哈希码在哈希表中更改时才有效。不幸的是,这个规则没有在你期望找到它的任何地方说明。文档错误?


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