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

Art*_*tem 20 java equals hashcode

我们正在编写一个类,它需要非常复杂的逻辑来计算equals()和hashCode().与...有关的东西:

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

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

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

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

编辑:这个问题不同于在Java中覆盖equals和hashCode时应该考虑哪些问题? 他们在那里询问一般指导,并且接受的答案不适用于我的情况.他们说"make equals和hashCode一致",在这里我问我究竟是怎么做的.

Dan*_*den 6

番石榴TESTLIB库有一个叫做类EqualsTester,可用于编写你的测试equals()hashCode()实现.

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


T.J*_*der 5

如果遍历算法足够复杂,您希望避免重复自己,请将算法分离为既可以使用equalshashCode可以使用的方法.

我看到两种选择,在广泛适用和有效之间进行权衡(通常情况下).

从广义上适用的

第一个选项是编写一个通用的遍历方法,它接受一个函数接口并在遍历的每个阶段回调它,因此你可以将一个lambda或实例传递给它,其中包含你想要在遍历时执行的实际逻辑; 该Visitor模式.该界面希望有一种方式来说"停止遍历"(例如,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;
}
Run Code Online (Sandbox Code Playgroud)

然后equalshashCode用它来实现平等检查或哈希码大厦,而不必知道遍历算法.

我选择上面的方法让方法返回一个标志,表示遍历是否提前结束,但这是一个设计细节.您可能不会返回任何东西,或者可能会返回this链接,无论您的情况适合什么.

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

具体而有效

...所以你可能想写一些特定于这种情况的东西,编写具有逻辑equalshashCode内置的东西.它将在使用时返回哈希码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;    
}
Run Code Online (Sandbox Code Playgroud)

具体而言,具体到您的情况以及您的风格指南等.有些人会other通过equals处理other == null案例本身使这个参数服务于两个目的(包括标志和"其他"对象),并且只有当它具有非null对象时才调用该工作者.我宁愿避免加倍这样的论点的含义,但你经常看到它.

测试

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

关于 hashCode

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

  • 这对我来说没有意义:`equals()`需要第二个对象来比较,但`hashCode()`没有.所以我没有看到任何简单的方法来为两者使用相同的代码路径.我绝对不鼓励在`equals()`或特别是`hashCode()`中进行任何分配,因为这些方法将从你不能控制的代码中调用,通常是在热路径上.例如,将一个不相关的对象添加到包含您的对象的`HashMap`可能会导致您的对象的`hashCode()`被调用 - 如果每秒发生数千次,那么您将创建大量的GC压力. (4认同)

plu*_*ash 5

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