equals 和 hashCode 的可重用实现

Reusable implementation of equals and hashCode

我正在做一个项目,其中许多 classes 需要 equalshashCode 的适当典型实现:每个 class 都有一组初始化的最终字段在构造 "deeply" 不可变对象时(null 在某些情况下旨在被接受)用于散列和比较。

为了减少样板代码的数量,我考虑编写一个摘要 class 提供此类行为的通用实现。

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

}

这基本上是 here 的提议,但有一些差异。

在我看来,这似乎是一种减少冗长并且仍然有效实现 equalshashCode 的简单而好的方法。但是,由于我不记得曾经见过类似的东西(除了上面链接的答案),我想在申请之前特别询问是否有反对这种方法的观点(或者可能有一些可以应用的改进)它贯穿整个项目。

我已经发现这种方法存在两个问题:

  1. 当且仅当它们的所有字段都匹配时,两个对象才被认为是相等的。在现实世界中情况并非总是如此。
  2. 通过将您的 classes 从 AbstractHashable 变成 extend,您将无法再从任何其他 classes 变成 extend。这是为重用 equalshashCode.
  3. 付出的相当高的代价

第一个问题可以通过将更多元数据传递给您的 AbstractHashable class 来解决,这样它就可以识别哪些字段是可选的。例如,您可以将另一个数组传递给 AbstractHashTable,其中包含要通过 setter 作为其元素忽略的索引位置。第二个问题可以通过使用 Composition 来解决。与其扩展 AbstractHashTable,不如重构它,以便它可以与其用户建立 HAS-A 关系,而不是 IS-A关系。

However, as I don't recall to have ever seen something similar (except in the answer linked above), I would like to specifically ask whether is there some point against this approach

这种方法肯定会影响代码的可读性。如果您能想出一种更具可读性的方法(比如使用注释),我认为重用 equalshashCode 实现没有任何问题。

综上所述,像 eclipse 这样的现代 IDE 可以很容易地为您生成 equalshashCode 实现,所以真的有必要想出这种解决方案吗?我相信没有.

根据我的经验,这似乎很糟糕,因为您会增加在运行时与编译时可能遇到的错误(请记住,在列表等中使用这些对象现在会给您带来意想不到的行为)。如果 类 中的字段顺序不同怎么办?其次,你滥用继承? Maybee 选择了一些框架(https://projectlombok.org/),它根据注释生成哈希码和 equals?

该解决方案应该有效。

不过从OOP的角度感觉有点弱:

  • 您正在复制数据(superclass 中的字段与对象中的字段),因此您的对象是原来的两倍大;另外,如果你需要让它们可变,你需要在两个地方维护状态
  • 强制class层级,只提供equals/hashCode

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

我建议看一下 apache 的 HashCodeBuilder and EqualsBuilder 类。它也有基于反射的方法,如 reflectionHashCode 和 reflectionEquals。