【问题标题】:Why HashSet sometimes doesn't add object when relying on default hash and equals?为什么HashSet在依赖默认hash和equals时有时不添加对象?
【发布时间】:2021-08-20 23:26:22
【问题描述】:

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

我见过的一些可能的理论:

  • HashMap 将根据底层容器的大小使用模数来确定放置它的位置。如果键不同,这可能会产生冲突,但我没有看到任何使用模数运算符的 HashMap 实例。

  • HashMap 将使用 hashCode 加上它自己的散列函数来确定一个键的散列。由于默认的 hashCode 可能并不总是返回相同的哈希,因此在 HashMap 的哈希函数中再次调用时可能会导致结果不稳定。我更倾向于相信这一点,因为在覆盖 hashCode 函数时会提供稳定的结果。

但是,我还没有找到明确的答案,为什么当使用来自 Object 的默认 hashCode 和 equals 函数时,HashSet#add 在添加包含不同字段且是通过新构造函数创建的唯一实例的对象时有时会返回 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);
    }

在键入此内容时,我意识到我正在扩展的类具有 Lombok 生成的 hash 和 equals,但继承的类没有方法。当我在继承类上添加 equals 和 hash 时,一切都是桃色的。仍然不是 100% 肯定 HashSet 不喜欢它的原因。

【问题讨论】:

  • 欢迎来到 Stack Overflow。请通过tour 了解 Stack Overflow 的工作原理,并阅读How to Ask 以了解如何提高问题的质量。然后edit你的问题将你的源代码包含为minimal reproducible example,可以被其他人编译和测试。
  • 还有第三种选择:你的代码做错了什么,或者其中有一个错误的假设。请发帖minimal reproducible example
  • 好吧。我想这里有经验的程序员已经给了你你需要的答案。 “还有第三种选择:你的代码做错了,或者其中有错误的假设。请发minimal reproducible example”。 不要把时间浪费在理论上......在没有任何支持证据的情况下发布。
  • 我愿意接受这可能是一些与 Lombok 相关的奇怪问题(但您将其作为 Java 问题提出......)。但是,您所看到的(而我没有看到!)更有可能实际上是由于您的实际代码中的一些错误或不正确的假设......这在您向我们展示的代码 sn-ps 中没有体现。
  • HashSet 使用equals()hashCode() 来检测两个对象的相等性。由于类MyClass 由于继承而没有实现它们,因此使用了来自超类MyHelperClass 的方法。

标签: java equals hashset hashcode


【解决方案1】:

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

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() 时才相等。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2023-03-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-01-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多