Java 如果hashcode不同,为什么HashSet允许相等的项?

Java 如果hashcode不同,为什么HashSet允许相等的项?,java,hashcode,hashset,Java,Hashcode,Hashset,该类有一个方法,该方法不是从另一个类继承的。该方法的Javadoc说明如下: 如果指定的元素尚未存在,则将其添加到此集合。更正式地说,如果此集合不包含元素e2,则将指定的元素e添加到此集合,从而(e==null?e2==null:e.equals(e2))。如果此集合已经包含元素,则调用将保持集合不变并返回false 换句话说,如果两个对象相等,则不会添加第二个对象,哈希集将保持不变。然而,我发现,如果对象e和e2具有不同的hashcode,则这是不正确的,尽管事实上e.equals(e2)。下

该类有一个方法,该方法不是从另一个类继承的。该方法的Javadoc说明如下:

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

换句话说,如果两个对象相等,则不会添加第二个对象,哈希集将保持不变。然而,我发现,如果对象
e
e2
具有不同的hashcode,则这是不正确的,尽管事实上
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)));
    }
正如上面的例子清楚地显示的,HashSet能够添加两个元素,其中
e.equals(e2)

我将假设这不是Java中的一个bug,并且事实上有一些完全合理的解释来解释为什么会这样。但我不知道到底是什么。我缺少什么?

因为
hashcode()
的实现非常糟糕

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

@Override
public int hashCode(){
    return new Random().nextInt();
}
每次对同一对象求值时,都会返回不同的has代码。显然你会得到错误的结果


add()函数如下所示

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

阅读更多有关短路的内容。由于
e.hash==hash
为false,因此不会计算任何其他值。


我希望这会有所帮助。

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

hashCode()
文档:

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

开始等于

请注意,每当重写
hashCode
方法时,通常需要重写该方法,以便维护
hashCode
方法的一般约定,该约定规定相同的对象必须具有相同的哈希代码

HashSet
依赖于一致地实现
equals
hashCode
HashSet
名称的
HashSet
部分基本上意味着“该类使用
hashCode
以提高效率。”如果这两种方法的实现不一致,则所有赌注都被取消


这不应该发生在真实代码中,因为您不应该违反真实代码中的约定…

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

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

和…

哈希代码实现违反了约定,即除非标识字段的对象发生更改,否则它不应更改

我想你真正想问的是:

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

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

我会在这里尽量做到这一点;我建议您重新表述您的问题,以消除对Java中糟糕文档/bug的指责,这样您就可以更直接地探究为什么会遇到您所看到的行为


文件说明(重点增加):

请注意,每当重写
hashCode
方法时,通常需要重写该方法,以便维护
hashCode
方法的一般约定,该约定规定相同的对象必须具有相同的哈希代码

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

如果查看(HashSet的
内部使用的)代码,您会注意到一个内部静态类
条目
,定义如下:

static class Entry<K,V> implements Map.Entry<K,V> {
  final K key;
  V value;
  Entry<K,V> next;
  int hash;
  ...
}
特别要注意的是,如果散列码相等,并且键不是完全相同的对象,则条件函数只调用
key.equals(k)
,原因是。根据这些方法的约定,
HashMap
跳过此调用应该是安全的。如果对象实现不正确,则
HashMap
所做的这些假设将不再正确,并且您将返回不可用的结果,包括集合中的“重复”


请注意,您的声明“HashSet…有一个
add(Object o)
方法,它不是从另一个类继承的”并不完全正确。虽然其父类,
AbstractSet
没有实现此方法,但父接口确实指定了该方法的约定。
Set
接口与哈希无关,只与相等有关,因此它指定了方法i的行为
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;
}
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
static class Entry<K,V> implements Map.Entry<K,V> {
  final K key;
  V value;
  Entry<K,V> next;
  int hash;
  ...
}
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
}