为什么 HashSet 在依赖默认哈希和等于时有时不添加对象?

Why HashSet sometimes doesn't add object when relying on default hash and equals?

我正在使用 ConcurrentHashMap<String, HashSet<MyClass>>,有时在向集合中添加新的 MyClass 时会失败。构造函数有 3 个参数,其中 2 个是相同的实例,但 1 个参数的实例和值不同。在 500 次测试执行的批运行期间,我发现使用 Java 的 Object 提供的默认散列和等于方法时的失败率为 0.5% - 18%。然而,当我自己生成它们或使用 Lombok 的 @EqualsAndHashCode 为我创建它们时,在超过 250k+ 的测试中我从未见过它失败。我深入研究了下面发生的事情,但找不到关于为什么有时 HashSet#add 会 return false 的可靠答案,即即使通过新构造函数调用 MyClass 也不会将对象添加到集合中。

我看到的一些可能的理论:

但是,我还没有找到关于为什么在使用对象的默认 hashCode 和 equals 函数时,添加包含不同字段的对象时 HashSet#add 有时 return false 和是通过新构造函数创建的唯一实例吗?

public class MyClass extends MyHelperClass {
    private String myString;

    public MyClass(String id, String id2) {
        super(id2);
        this.myString = id;
    }
}
public class MyHelperClass {
   private String myString;

    MyHelperClass(String myString_){
        this.myString = myString_;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        MyHelperClass that = (MyHelperClass) o;
        return Objects.equals(myString, that.myString);
    }

    @Override
    public int hashCode() {
        return Objects.hash(myString);
    }
}
public class SomeService {    
public final Map<String, Set<MyClass>> myMap = new ConcurrentHashMap<>();

public void addThis(String id1, String id2) {
        myMap.computeIfAbsent(id1, a -> new HashSet<>());
        myMap.get(id1).add(new MyClass(id2, id1));
    }
}

我正在运行的测试:

    SomeService someService = new SomeService();

    void test() {
        someService.addThis("id1", "id2");
        someService.addThis("id1", "id3");
        assertThat(someService.myMap.get("id1").size()).isEqualTo(2);
    }

在输入时,我意识到我正在扩展的 class 具有 Lombok 生成的散列和等式,但继承的 class 没有这些方法。当我在继承 class 上添加 equals 和 hash 时,一切都很好。仍然不是 100% 肯定为什么 HashSet 不喜欢它。

您的测试代码中的 SomeService class 最终将生成以下对象:

new MyClass("id2", "id1");
new MyClass("id3", "id1");

由于继承,将为这两个对象调用以下基本构造函数:

new MyHelperClass("id1");
new MyHelperClass("id1");

如您所见,您使用相同的参数调用基本构造函数。并且根据您的 equals() 实现,它们是相同的(并且具有相同的哈希码)。检查以下示例代码以显示问题:

MyClass o1 = new MyClass("id2", "id1");
MyClass o2 = new MyClass("id3", "id1");
System.out.println(o1.hashCode());
System.out.println(o2.hashCode());
System.out.println(o1.equals(o2));

这将生成以下输出:

104085
104085
true

因为它们相等,只有一次实例将保存在您的 HashSet()