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

2022-09-02 10:38:09

我们正在编写一个类,它需要非常复杂的逻辑来计算 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个相等的对象将转到不同的桶,它们永远不会被等于()进行比较。在这里,通过“不同步”,我的意思是它们进入不同的桶。

确保相等和哈希码不会不同步的最佳方法是什么?

编辑:这个问题与在Java中覆盖等于和哈希码时应考虑哪些问题不同?在那里,他们询问通用指南,接受的答案不适用于我的情况。他们说“使相等和哈希代码一致”,在这里我问我到底该怎么做。


答案 1

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

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


答案 2

如果遍历算法足够复杂,您希望避免重复自己,请将该算法隔离为一个既可以使用又可以使用的方法。equalshashCode

我看到了两种选择,它们(就像经常发生的情况一样)在广泛适用和高效之间进行权衡。

广泛适用

第一种选择是编写一个相当通用的遍历方法,该方法接受功能接口并在遍历的每个阶段回调它,因此您可以将lambda或实例传递到其中包含要在遍历时执行的实际逻辑;访客模式。该接口希望有一种方法可以说“停止遍历”(例如,当它知道答案“不相等”时,也可以保释)。从概念上讲,这看起来像这样: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

我在上面选择让方法返回一个标志,说明遍历是否提前结束,但这是一个设计细节。您可能不会返回任何内容,或者可能会返回链接,无论您的情况如何。this

但问题是,使用它意味着分配一个实例(或使用lambda,但随后您可能需要为lamba分配一些东西来更新,以跟踪它正在做什么)并执行大量方法调用。也许这在你的情况下很好;也许这是一个性能杀手,因为你的应用需要使用很多。:-)equals

具体而高效

...因此,您可能希望编写特定于此案例的内容,编写具有逻辑并内置于其中的内容。当 被 使用时,它将返回哈希代码,或者返回 (0 = 不等于,!0 = 等于)的标志值。不再通常有用,但它避免了创建访客实例以传入/ lambda开销/ 调用开销。从概念上讲,这可能看起来像这样:equalshashCodehashCodeequals

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;    
}

同样,具体细节将是,嗯,特定于您的情况以及您的风格指南等。有些人会通过处理案例本身并使参数达到两个目的(标志和“其他”对象),并且仅在它具有非对象时才调用此工作线程。我宁愿避免在这样的论点的含义上加倍努力,但你经常看到它。otherequalsother == nullnull

测试

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

附注关于hashCode

无论上述情况如何,如果您预计会被大量调用,则可以考虑将结果缓存到实例字段。如果您要处理的对象是可变的(听起来像是可变的),那么每次更改对象的状态时,您都会使该存储的哈希码失效。这样,如果对象未更改,则不必在后续调用 中重复遍历。但是,当然,如果您忘记使哈希代码失效,即使是其中一个突变体方法...hashCodehashCode


推荐