如何保证 equals() 和 hashCode() 是同步的?

How to guarantee that equals() and hashCode() are in sync?

我们正在编写一个class,它需要非常复杂的逻辑来计算equals() 和hashCode()。符合以下内容的内容:

@Getters @Setters @FieldDefaults(level=AccessLevel.PRIVATE)
public class ExternalData {
  TypeEnum type;
  String data;
  List<ExternalData> children;
} 

我们不构造这些对象,它们是从外部复杂系统 XML 反序列化的。有 20 多种类型,根据类型数据可以忽略,或处理子项,或不处理子项,每种类型节点的数据比较取决于类型。

我们创建了 equals() 和 hashCode() 来反映所有这些规则,但最近 运行 遇到了一个问题,即 hashCode 与 equals 不同步,导致相等的对象被添加到 HashSet 两次。 我相信 HashMap(和 HashSet 就此而言)在 Java 中以这种方式实现:https://en.wikipedia.org/wiki/Hash_table 该实现首先根据 hashCode 将对象放入桶中,然后为每个桶检查是否相等。在不幸的情况下,2 个相等的对象将进入不同的桶,它们将永远不会被 equals() 进行比较。 "Out of sync" 在这里我的意思是他们进入不同的桶。

确保 equals 和 hashCode 不同步的最佳方法是什么?

编辑: 这个问题不同于 What issues should be considered when overriding equals and hashCode in Java? 他们在那里询问一般指导,接受的答案不适用于我的情况。他们说 "make equals and hashCode consistent",我在这里问我到底该怎么做。

如果遍历算法足够复杂以至于您想避免自己重复,请将算法隔离为 equalshashCode 都可以使用的方法。

我看到了两个选项,它们(通常是这种情况)在广泛适用和高效之间进行权衡。

广泛适用

第一个选择是编写一个非常通用的遍历方法,它接受一个功能接口并在遍历的每个阶段回调它,这样你就可以将一个 lambda 或实例传递给它,其中包含你想要的实际逻辑遍历时执行; Visitor pattern。该接口希望有一种方法来表示 "stop traversing"(例如,当它知道答案是 "not equal" 时,equals 可以放弃)。 概念上,看起来像:

private boolean traverse(Visitor visitor) {
    while (/*still traversing*/) {
        if (!visitor.visitNode(thisNode)) {
            return false;
        }
        /*determine next node to visit and whether done*/
    }
    return true;
}

然后 equalshashCode 使用它来实现相等性检查或哈希码构建,而无需知道遍历算法。

我在上面选择了方法 return 作为遍历是否提前结束的标志,但这是一个设计细节。你可能不会 return 任何东西,或者可能 return this 链接,任何适合你的情况。

不过,问题是使用它意味着分配一个实例(或使用 lambda,但随后您可能需要为 lambda 分配一些内容以进行更新以跟踪它在做什么)并做很多事情方法调用。也许这对你来说很好;也许它是性能杀手,因为您的应用需要经常使用 equals。 :-)

具体高效

...所以你可能想写一些特定于这种情况的东西,写一些内置了 equalshashCode 逻辑的东西。它将 return 被 hashCode 使用时的哈希码,或 equals 的标志值(0 = 不等于,!0 = 等于)。不再普遍有用,但它避免创建访问者实例以传入/lambda 开销/调用开销。 概念上,这可能类似于:

private int equalsHashCodeWorker(Object other, boolean forEquals) {
    int code = 0;

    if (forEquals && other == null) {
        // not equal
    } else {
        while (/*still traversing*/) {
            /*update `code` depending on the results for this node*/
        }
    }

    return code;    
}

同样,细节将是,嗯,特定于您的案例以及您的风格指南等。有些人会让 other 参数有两个目的(标志和 "other" 对象),方法是让 equals 自己处理 other == null 情况,并且只在它有时调用这个工人非 null 对象。我宁愿避免把这样的论点的含义加倍,但你经常看到它。

测试

无论你走哪条路,如果你在一家有测试文化的商店里,你自然会想要为你已经看到失败的复杂案例以及你看到失败机会的其他案例构建测试.

关于 hashCode

的附注

尽管如此,如果您希望 hashCode 被多次调用,您可以考虑将结果缓存到实例字段中。如果您正在执行此操作的对象是可变的(听起来确实如此),则只要您改变对象的状态,您就会使存储的哈希码无效。这样,如果对象没有改变,则不必在后续调用 hashCode 时重复遍历。但是,当然,如果您甚至忘记使 一个 中的哈希码无效...

Guava testlib 库有一个名为 EqualsTester 的 class,可用于为您的 equals()hashCode() 实现编写测试。

添加测试既可以帮助您确保代码现在是正确的,也可以确保它保持正确if/when您将来修改它。

要考虑的一个选项可能是代码生成。基本上,您写出需要比较的事物列表,并有一个程序可以生成 equals 方法和 hashcode 方法。由于这两种方法都是从相同的要比较的事物列表中生成的,因此它们不应该不同步(当然前提是各个元素不同步)。

如果a.equals(b),这意味着a.hashcode() == b.hashcode()

不过,小心!a.equals(b) NOT 意味着 a.hashcode() != b.hashcode().

这仅仅是因为哈希冲突可能是一个严重的问题,具体取决于您的算法和大量因素。一般来说,如果两个对象相等,它们的哈希码将总是相等。但是,您不能仅通过比较哈希码来确定两个对象是否相等,因为 a.hashode() == b.hashcode() 也确实 not 暗示 a.equals(b).