繁体   English   中英

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

[英]Why does HashSet allow equal items if hashcodes are different?

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

如果指定的元素尚不存在,则将其添加到此集合中。 更正式地,如果此集合不包含元素e2 (e==null ? e2==null : e.equals(e2)) ,则将指定元素e添加到此集合。 如果此set已包含该元素,则调用将保持set不变并返回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中的错误,实际上有一些完全合理的解释为什么会这样。 但我无法弄清楚到底是什么。 我错过了什么?

我想你真正要问的是:

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

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

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


equals()文档说明(强调添加):

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

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

如果你看一下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
}

特别注意,由于短路评估 ,如果哈希码相等且密钥不是完全相同的对象,则条件只调用key.equals(k) 通过这些方法的契约, HashMap跳过此调用应该是安全的。 如果你的对象被错误地实现,那么HashMap做出的这些假设就不再正确,你将得到不可用的结果,包括你的集合中的“重复”。


请注意,您的声明“ HashSet ...有一个add(Object o)方法,它不是从另一个类继承的 ”,这不太正确。 虽然它的父 AbstractSet没有实现此方法,但父接口 Set确实指定了方法的契约。 Set接口不关注哈希,只关注相等,因此它根据等式(e==null ? e2==null : e.equals(e2))指定此方法的行为。 只要您遵循合同, HashSet就会按照文档记录,但尽可能避免实际浪费的工作。 但是,一旦违反规则,就不能指望HashSet以任何有用的方式运行。

还要考虑如果您尝试使用错误实现的ComparatorTreeSet存储对象,您同样会看到无意义的结果。 我记录了一些TreeSet在另一个问题中使用不可靠的Comparator时的行为示例: 如何在Java中为StringBuffer类实现比较器以便在TreeSet中使用?

你基本违反了equals / hashCode的契约:

来自hashCode()文档:

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

并从equals

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

HashSet依赖于equalshashCode一致地实现 - 名称HashSetHash部分基本上暗示“此类使用hashCode来提高效率”。 如果两种方法没有一致地实施,那么所有的赌注都会被取消。

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

@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,因此不会评估任何其他内容。

我希望这有帮助。

因为hashcode()实际上非常糟糕

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM