Java 为什么赢了';t我的HashSet允许我添加两个相同的实例,如果它们的equals()表示它们';你错了吗?

Java 为什么赢了';t我的HashSet允许我添加两个相同的实例,如果它们的equals()表示它们';你错了吗?,java,set,equals,hashset,Java,Set,Equals,Hashset,说 如果指定的元素尚未存在,则将其添加到此集合。更正式地说,如果此集合不包含元素e2,则将指定的元素e添加到此集合,从而(e==null?e2==null:e.equals(e2))。如果此集合已经包含元素,则调用将保持集合不变并返回false 由于我下面的代码将为e.equals(e2)返回false,因此我希望它允许我添加相同的实例两次。但是集合只包含我的实例一次。有人能解释一下原因吗 package com.sandbox; import java.util.HashSet; impor

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

由于我下面的代码将为
e.equals(e2)
返回false,因此我希望它允许我添加相同的实例两次。但是集合只包含我的实例一次。有人能解释一下原因吗

package com.sandbox;

import java.util.HashSet;
import java.util.Set;

public class Sandbox {

    public static void main(String[] args) {
        Set<A> as = new HashSet<A>();
        A oneInstance = new A();
        System.out.println(oneInstance.equals(oneInstance));    //this prints false
        as.add(oneInstance);
        as.add(oneInstance);
        System.out.println(as.size());  //this prints 1, I'd expect it to print 2 since the System.out printed false
    }

    private static class A {
        private Integer key;

        @Override
        public boolean equals(Object o) {
            if (!(o instanceof A)) {
                return false;
            }

            A a = (A) o;

            if (this.key == null || a.key == null) {
                return false;   //the key is null, it should return false
            }

            if (key != null ? !key.equals(a.key) : a.key != null) {
                return false;
            }

            return true;
        }

        @Override
        public int hashCode() {
            return key != null ? key.hashCode() : 0;
        }
    }

}
package.com.sandbox;
导入java.util.HashSet;
导入java.util.Set;
公共类沙箱{
公共静态void main(字符串[]args){
Set as=新的HashSet();
A oneInstance=newa();
System.out.println(oneInstance.equals(oneInstance));//此打印为false
as.add(一个实例);
as.add(一个实例);
System.out.println(as.size());//这个打印1,我希望它打印2,因为System.out打印为false
}
专用静态A类{
私钥;
@凌驾
公共布尔等于(对象o){
如果(!(o实例A)){
返回false;
}
A=(A)o;
if(this.key==null | | a.key==null){
return false;//密钥为null,应该返回false
}
如果(key!=null?!key.equals(a.key):a.key!=null){
返回false;
}
返回true;
}
@凌驾
公共int hashCode(){
return key!=null?key.hashCode():0;
}
}
}

我不知道确切的原因,但我觉得有必要指出,当您实现equals时,equals方法的契约的一部分,您应该坚持的是它是自反的,这意味着相同的对象等于自身。所以你的等式应该是真的

我想回答您的问题,当您只将同一实例的两个项添加到HashSet中时,.equals()方法不会被调用。可能只有hashCode被调用到那个点。因为hashCode返回键,所以它每次都会返回相同的键,并且该项只是被散列到相同的位置两次,在集合中只剩下一项。

HashSet(实际上是隐藏的HashMap)有一个“优化”,它在调用
equals()
方法之前检查对象引用是否相等。由于将同一实例放入两次,因此即使
equals()
方法不一致,它们也被视为相等

HashMap.put()中的相关行:


你违反了的合同,合同开始于:

equals方法在非null对象引用上实现等价关系:

  • 它是自反的:对于任何非空参考值x,
    x.equals(x)
    应该返回true
正如您的示例代码所示:

System.out.println(oneInstance.equals(oneInstance)); //this prints false
似乎
HashSet
(完全合理地)假设了自反性,当它发现完全相同的对象已经在集中时,作为一种优化,它不会检查是否相等。因此,它甚至不会调用
equals
方法-它认为对象已经在集合中,因此不添加第二个副本

特别是,如果
x.equals(x)
为false,则任何包含检查也将无效

我会像这样实现
equals

public boolean equals(Object o) {
    // Normal reflexive optimization
    if (this == o) {
        return true;
    }

    // "Correct type" check
    if (!(o instanceof A)) {
        return false;
    }
    
    A a = (A) o;

    // If both keys are null, the objects are equal. This is the most normal
    // approach; you *could* make non-identical objects with null keys non-equal,
    // but that would be odd.
    if (this.key == null && a.key == null) {
        return true;
    }

    // If exactly *one* key is null, the objects are not equal.
    if (this.key == null || a.key == null) {
        return false;
    }

    // By now we know that both keys are non-null; use normal equality.
    return this.key.equals(a.key);
}
或者,如果您使用的是Java 7:

public boolean equals(Object o) {
    // Normal reflexive optimization
    if (this == o) {
        return true;
    }

    // "Correct type" check
    if (!(o instanceof A)) {
        return false;
    }
    
    A a = (A) o;
    return Objects.equals(this.key, a.key);
}

散列映射/表的工作原理是:获取一个对象,并使用“散列”函数对其进行“散列”,以生成一个Psuedo随机均匀分布的唯一id,该id表示该对象,其中所述id可用作索引结构(如数组)中的键。理想情况下,您会有一个完美的散列,其中每个唯一的项都会生成一个唯一的可索引id

显然,您的数组大小是固定的(您可以增加数组,但这将显著影响运行时性能),因此在某个时候,如果您继续向哈希映射/表添加元素,最终将得到两个具有相同哈希代码的项,然后将发生冲突;这就是平等发挥作用的地方

当出现这种情况时,等式用于通过迭代(通常是通过在索引位置而不仅仅是元素处存储LinkedList)可用对象并检查equals方法来消除对正在查找的键/值的歧义

因此,您案例中的问题很简单:如果您的哈希实现是错误的,那么HashSet(由HashMap支持)无法在其表中找到您的对象,因此从不费心调用equals(查看HashMap.get()以查看其实现)


如果您想让equals中使用的内容起作用,那么必须在hashCode()中使用,反之亦然。如果实现equals(),那么实现hashCode()是个非常好的主意。如果实现hashCode(),则必须实现equals才能使哈希真正起作用。

问题不在
HashSet
中,而在
equals
方法中,该方法不遵循
equals
约定。@tietyty:“它是自反的:对于任何非空引用值x,x.equals(x)应该返回true。”(From:
HashSet
(或
HashMap
)假定
对象。equals
是按照
对象
中的指定实现的。特别是与它首先检查的
==
一致,可能是为了效率。@tieTYT源代码是这么说的(UTSL)。有人想说我为什么会被否决吗?我认为这是一个非常合理的问题。它清楚地解释了它的要求,它给出了示例代码,甚至有注释。不,这不是哈希代码的使用方式。在一个哈希集中,可以有许多项使用相同的哈希代码。一旦出现哈希冲突,
等于()
方法被使用。@SotiriosDelimanolis在正常情况下,是的。但在本例中不是。您可以试试
public boolean equals(Object o) {
    // Normal reflexive optimization
    if (this == o) {
        return true;
    }

    // "Correct type" check
    if (!(o instanceof A)) {
        return false;
    }
    
    A a = (A) o;
    return Objects.equals(this.key, a.key);
}