重用equals和hashCode的实现

5

我正在处理一个项目,许多类需要适当的典型实现equalshashCode:每个类都有一组在构造时初始化的“深度”不可变对象(在某些情况下可以接受null),用于哈希和比较。

为了减少样板代码的数量,我考虑编写一个抽象类,提供这种行为的常见实现。

public abstract class AbstractHashable {

    /** List of fields used for comparison. */
    private final Object[] fields;

    /** Precomputed hash. */
    private final int hash;

    /**
     * Constructor to be invoked by subclasses.
     * @param fields list of fields used for comparison between objects of this
     * class, they must be in constant number for each class
     */
    protected AbstractHashable(Object... fields) {
        this.fields = fields;
        hash = 31 * getClass().hashCode() + Objects.hash(fields);
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == this) {
            return true;
        }
        if (obj == null || !getClass().equals(obj.getClass())) {
            return false;
        }
        AbstractHashable other = (AbstractHashable) obj;
        if (fields.length != other.fields.length) {
            throw new UnsupportedOperationException(
                    "objects of same class must have the same number of fields");
        }
        for (int i=0; i<fields.length; i++) {
            if (!fields[i].equals(other.fields[i])) {
                return false;
            }
        }
        return true;
    }

    @Override
    public int hashCode() {
        return hash;
    }

}

这是打算这样使用的:

public class SomeObject extends AbstractHashable {

    // both Foo and Bar have no mutable state
    private final Foo foo;
    private final Bar bar;

    public SomeObject(Foo foo, Bar bar) {
        super(foo, bar);
        this.foo = Objects.requireNonNull(foo);
        this.bar = bar; // null supported
    }

    // other methods, no equals or hashCode needed

}

这基本上是与此处提出的方案有所不同,但与之相似。

这对我来说是一种简单而好的方法,可以减少冗余,并仍然具有equalshashCode的高效实现。然而,由于我不记得曾经见过类似的东西(除了上面链接的答案),在将其应用到整个项目之前,我想特别询问是否有反对这种方法的观点(或者可能适用的改进方法)。


1
我已经发现两个问题。1. 只有当所有字段匹配时,两个对象才会被视为相等。这可能并不总是你想要的。2. 一个类只能继承一个其他类。你真的想要锁定你的类,使其无法继承其他类,因为equalshashCode的重用更重要吗? - Chetan Kinger
4个回答

5
我已经发现这种方法存在两个问题:
  1. 只有当两个对象的所有字段都匹配时,它们才被视为相等。在现实世界中,情况并非总是如此。
  2. 通过使你的类 extendAbstractHashable,你不能再从其他任何类继承。这对于复用 equalshashCode 来说是一个相当高的代价。

第一个问题可以通过向你的 AbstractHashable 类传递更多元数据来解决,从而使其能够识别哪些字段是可选的。例如,你可以通过 setter 将另一个数组作为参数传递到 AbstractHashTable 中,作为要忽略的索引位置。第二个问题可以通过使用 组合 来解决。不要继承 AbstractHashTable,而是重构它,使它能够与其用户建立 HAS-A 关系,而不是 IS-A 关系。

然而,我不记得曾经见过类似的东西(除了上述链接中的答案),所以我想特别问一下是否存在反对这种方法的观点

这种方法肯定会影响你代码的可读性。如果你能想出一个更易读的方法(例如使用注释),那么我认为复用 equalshashCode 实现并没有什么问题。

话虽如此,现代 IDE(如 Eclipse)可以轻松为你生成 equalshashCode 实现,那么是否真的有必要想出这种解决方案呢?我相信 没有


1
谢谢,这两个观点很有意思(尤其是第一个,我没有想到)。当我提出这个问题时,我正在使用Eclipse的自动生成功能:即使是自动生成的样板代码,我也不喜欢它(还因为像添加字段这样的更改变得更容易出错),但我现在可能会处理它。 - rrobby86

0
根据我的经验,这似乎不好,因为您将增加运行时与编译时可能出现的错误(请记住,这些对象在列表等中的所有使用现在都可能导致意外行为)。如果类中字段的顺序不同怎么办?其次,您是否误用了继承?也许可以选择一些基于注释生成哈希码和equals的框架(https://projectlombok.org/)。

如果类中字段的顺序不同怎么办?这永远不会成为问题,因为子类(例如SomeObject)中的构造函数始终会强制执行一个固定的顺序来初始化字段。此外,您正在比较同一类别的对象,所以字段顺序会如何更改呢? - Chetan Kinger
你是对的,你正在比较同一个类的对象,但是为什么还要有 if (fields.length != other.fields.length) 这个条件判断呢? - HoleInVoid
OP已经添加了检查极端情况的代码,以防开发人员使用不同的构造函数来实例化对象。 - Chetan Kinger
但是这些字段仍然会存在吗? - HoleInVoid

0

这个解决方案应该是可行的。

然而,从面向对象编程的角度来看,它有点薄弱:

  • 你正在复制数据(超类中的字段与对象中的字段),因此你的对象大小增加了一倍;如果你需要使它们可变,你需要在两个地方维护状态
  • 类层次结构是被迫的,只是为了提供equals/hashCode方法

现代IDE可以很容易地生成这些方法,如果你想要,你也可以使用框架(如Lombok)或库(如Guava的Objects/Java 8的Objects)来简化这些方法。


0
我建议您查看Apache的HashCodeBuilderEqualsBuilder类。它具有基于反射的方法,如reflectionHashCode和reflectionEquals。

谢谢您的建议。然而,反射并不是最优的性能选择,我认为对于具有大量依赖对象图的对象可能会很明显(这在我的情况下可能会发生)。 - rrobby86

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接