为什么 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 也不会将对象添加到集合中。
我看到的一些可能的理论:
HashMap 将根据底层容器的大小使用模数来确定放置它的位置。如果键不同,这可能会产生冲突,但我没有看到任何使用模数运算符的 HashMap 实例。
HashMap会使用hashCode加上自己的哈希函数来确定key的哈希值。由于默认的 hashCode 可能并不总是 return 相同的哈希值,因此在 HashMap 的哈希函数中再次调用时可能会导致结果不稳定。我更倾向于相信这一点,因为重写 hashCode 函数会提供稳定的结果。
但是,我还没有找到关于为什么在使用对象的默认 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()
。
我正在使用 ConcurrentHashMap<String, HashSet<MyClass>>
,有时在向集合中添加新的 MyClass 时会失败。构造函数有 3 个参数,其中 2 个是相同的实例,但 1 个参数的实例和值不同。在 500 次测试执行的批运行期间,我发现使用 Java 的 Object 提供的默认散列和等于方法时的失败率为 0.5% - 18%。然而,当我自己生成它们或使用 Lombok 的 @EqualsAndHashCode
为我创建它们时,在超过 250k+ 的测试中我从未见过它失败。我深入研究了下面发生的事情,但找不到关于为什么有时 HashSet#add
会 return false 的可靠答案,即即使通过新构造函数调用 MyClass 也不会将对象添加到集合中。
我看到的一些可能的理论:
HashMap 将根据底层容器的大小使用模数来确定放置它的位置。如果键不同,这可能会产生冲突,但我没有看到任何使用模数运算符的 HashMap 实例。
HashMap会使用hashCode加上自己的哈希函数来确定key的哈希值。由于默认的 hashCode 可能并不总是 return 相同的哈希值,因此在 HashMap 的哈希函数中再次调用时可能会导致结果不稳定。我更倾向于相信这一点,因为重写 hashCode 函数会提供稳定的结果。
但是,我还没有找到关于为什么在使用对象的默认 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()
。