如果哈希码不同,为什么HashSet允许相等的项?

时间:2014-01-06 06:53:16

标签: java hashcode hashset

HashSet类有一个add(Object o)方法,它不是从另一个类继承的。该方法的Javadoc说明如下:

  

如果指定的元素尚不存在,则将其添加到此集合中。更正式地说,如果此集合不包含e元素e2,则将指定的元素(e==null ? e2==null : e.equals(e2))添加到此集合中。如果此集已包含元素,则调用将保持集不变并返回false

换句话说,如果两个对象相等,则不会添加第二个对象,并且HashSet将保持不变。但是,我发现如果对象ee2具有不同的哈希码,则不是这样,尽管事实是e.equals(e2)。这是一个简单的例子:

import java.util.HashSet;
import java.util.Iterator;
import java.util.Random;

public class BadHashCodeClass {

    /**
     * A hashcode that will randomly return an integer, so it is unlikely to be the same
     */
    @Override
    public int hashCode(){
        return new Random().nextInt();
    }

    /**
     * An equal method that will always return true
     */
    @Override
    public boolean equals(Object o){
        return true;
    }

    public static void main(String... args){
        HashSet<BadHashCodeClass> hashSet = new HashSet<>();
        BadHashCodeClass instance = new BadHashCodeClass();
        System.out.println("Instance was added: " + hashSet.add(instance));
        System.out.println("Instance was added: " + hashSet.add(instance));
        System.out.println("Elements in hashSet: " + hashSet.size());

        Iterator<BadHashCodeClass> iterator = hashSet.iterator();
        BadHashCodeClass e = iterator.next();
        BadHashCodeClass e2 = iterator.next();
        System.out.println("Element contains e and e2 such that (e==null ? e2==null : e.equals(e2)): " + (e==null ? e2==null : e.equals(e2)));
    }

主要方法的结果是:

Instance was added: true
Instance was added: true
Elements in hashSet: 2
Element contains e and e2 such that (e==null ? e2==null : e.equals(e2)): true

如上面的示例清楚地显示,HashSet能够在e.equals(e2)中添加两个元素。

我将假设这是不是 Java中的一个错误,并且实际上有一些非常合理的解释为什么会这样。但我无法弄清楚到底是什么。我错过了什么?

5 个答案:

答案 0 :(得分:12)

我认为你真正要问的是:

“为什么HashSet添加具有不等哈希码的对象,即使它们声称是相等的?”

我的问题和你发布的问题之间的区别在于你假设这种行为是一个错误,因此从这个角度来看,你会感到悲伤。我认为其他海报已经完全解释了为什么这不是一个错误,但是它们没有解决潜在的问题。

我会在这里尝试这样做;我建议改述你的问题,以删除Java中糟糕的文档/错误的指控,这样你就可以更直接地探索为什么你正在遇到你正在看到的行为。


equals()文件说明(强调增加):

  

请注意,一旦覆盖此方法,通常需要覆盖hashCode方法,以便维护hashCode方法的常规协定,该方法指出相等的对象必须具有相等的哈希码

equals()hashCode()之间的契约不仅仅是Java规范中令人讨厌的怪癖。它在算法优化方面提供了一些非常有价值的好处。通过假设a.equals(b)暗示a.hashCode() == b.hashCode(),我们可以进行一些基本的等价测试而无需直接调用equals()。特别是,can be turned around - a.hashCode() != b.hashCode()上方的不变量意味着a.equals(b)将为假。

如果您查看HashMap的代码(HashSet在内部使用),您会注意到内部静态类Entry,其定义如下:

static class Entry<K,V> implements Map.Entry<K,V> {
  final K key;
  V value;
  Entry<K,V> next;
  int hash;
  ...
}

HashMap存储密钥的哈希码以及密钥和值。由于哈希码在密钥存储在地图中的时间内不会发生变化(请参阅Map的文档,“如果对象的值发生更改,则不会指定地图的行为以对象是地图中的键的方式影响等于比较的方式。“)HashMap缓存此值是安全的。通过这样做,它只需要为地图中的每个键调用hashCode()一次,而不是每次检查密钥时。

现在让我们看看put()的实现,我们看到这些缓存的哈希值被利用,以及上面的不变量:

public V put(K key, V value) {
  ...
  int hash = hash(key);
  int i = indexFor(hash, table.length);
  for (Entry<K,V> e = table[i]; e != null; e = e.next) {
    Object k;
    if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
      // Replace existing element and return
    }
  }
  // Insert new element
}

特别注意,由于short-circuit evaluation,如果哈希码相等且密钥不是完全相同的对象,则条件只会调用key.equals(k)。通过这些方法的合同,HashMap跳过此调用应该是安全的。如果您的对象未正确实现,HashMap所做的这些假设将不再适用,您将返回无法使用的结果,包括集合中的“重复”。


请注意,您的声明“ HashSet ...具有add(Object o)方法,该方法不是从其他类继承的”并不完全正确。虽然其父AbstractSet未实现此方法,但父接口Set确实指定了方法的合同。 Set接口不关注哈希,只关注相等,因此它根据与(e==null ? e2==null : e.equals(e2))的相等性来指定此方法的行为。只要你遵守合同,HashSet就像记录的那样工作,但尽可能避免实际做浪费的工作。但是,一旦违反规则,就不能指望HashSet以任何有用的方式行事。

另请注意,如果您尝试将对象存储在TreeSet中且实施不正确的Comparator,您同样会看到无意义的结果。我在一个问题中记录了TreeSet在另一个问题中使用不值得信任的Comparator时的行为示例:how to implement a comparator for StringBuffer class in Java for use in TreeSet?

答案 1 :(得分:8)

您基本违反了equals / hashCode的合同:

来自hashCode()文档:

  

如果两个对象根据equals(Object)方法相等,则对两个对象中的每一个调用hashCode方法必须产生相同的整数结果。

equals

  

请注意,一旦覆盖此方法,通常需要覆盖hashCode方法,以便维护hashCode方法的常规协定,该方法声明相等对象必须具有相等的哈希码

HashSet依赖equalshashCode一致实施 - Hash部分名称HashSet基本上暗示“此课程使用{{1}出于效率目的。“如果两种方法一致地实施,则所有投注均已关闭。

这不应该在实际代码中发生,因为你不应该在实际代码中违反合同......

答案 2 :(得分:2)

@Override
public int hashCode(){
    return new Random().nextInt();
}

每次评估时,您都会返回相同对象的不同代码。显然你会得到错误的结果。


add()函数如下

public boolean add(E e) {
return map.put(e, PRESENT)==null;
}

和put()是

public V put(K key, V value) {
    if (key == null)
        return putForNullKey(value);
    int hash = hash(key.hashCode());
    int i = indexFor(hash, table.length);
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
            V oldValue = e.value;
            e.value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

    modCount++;
    addEntry(hash, key, value, i);
    return null;
}

如果您注意到第一次计算的情况与您的情况不同,这就是添加对象的原因。只有当对象的哈希值相同时才会出现equals(),即碰撞已经发生。因为在散列不同的情况下,equals()永远不会执行

if (e.hash == hash && ((k = e.key) == key || key.equals(k)))

详细了解短路是什么。由于e.hash == hash为false 未评估任何其他内容。

我希望这会有所帮助。

答案 3 :(得分:0)

因为hashcode()确实非常实施

它将尝试在每个add()的每个随机存储桶中等同,如果您从hashcode()返回常量值,则不会让您输入任何

答案 4 :(得分:0)

不要求所有元素的哈希码都不同!只需要两个元素不相等。

首先使用HashCode查找对象应占用的哈希桶。如果hadhcodes不同,则假定对象不相等。如果哈希码相等,则equals()方法用于确定相等性。 hashCode的使用是一种效率机制。

和...
您的哈希代码实现违反了它不应该更改的合同,除非标识字段的对象发生更改。