Apache Commons equals/hashCode 构建器

161
我很好奇,这里的人对于使用org.apache.commons.lang.builder EqualsBuilder/HashCodeBuilder来实现equals/hashCode有什么看法?相比编写自己的代码,这种做法是否更好?它是否与Hibernate兼容?你有什么意见?

16
不要被reflectionEqualsreflectionHashcode函数所诱惑,它们会极大地影响性能。 - skaffman
15
昨天我在这里看到一些关于equals的讨论,然后有一些空闲时间,所以我进行了一个快速测试。我有4个具有不同equals实现的对象:Eclipse生成的、EqualsBuilder.append、EqualsBuilder.reflection和Pojomatic注释。基准是Eclipse。EqualsBuilder.append花费了3.7倍的时间。Pojomatic花费了5倍的时间。反射-based的花费了25.8倍的时间。这非常令人沮丧,因为我喜欢基于反射的简单性,而且我无法忍受“Pojomatic”这个名字。 - digitaljoel
5
另一个选择是Project Lombok;它使用字节码生成而不是反射,因此应该与Eclipse生成的代码一样高效。http://projectlombok.org/features/EqualsAndHashCode.html - Miles
8个回答

216

commons/lang的构建工具非常棒,我已经使用了几年而没有注意到性能开销(使用hibernate和不使用hibernate都一样)。但正如Alain所说,Guava的方式更加优雅:

这是一个示例Bean:

public class Bean{

    private String name;
    private int length;
    private List<Bean> children;

}

以下是使用Commons/Lang实现的equals()和hashCode()方法:

@Override
public int hashCode(){
    return new HashCodeBuilder()
        .append(name)
        .append(length)
        .append(children)
        .toHashCode();
}

@Override
public boolean equals(final Object obj){
    if(obj instanceof Bean){
        final Bean other = (Bean) obj;
        return new EqualsBuilder()
            .append(name, other.name)
            .append(length, other.length)
            .append(children, other.children)
            .isEquals();
    } else{
        return false;
    }
}

在Java 7或更高版本中(受Guava启发),可以这样做:
@Override
public int hashCode(){
    return Objects.hash(name, length, children);
}

@Override
public boolean equals(final Object obj){
    if(obj instanceof Bean){
        final Bean other = (Bean) obj;
        return Objects.equals(name, other.name)
            && length == other.length // special handling for primitives
            && Objects.equals(children, other.children);
    } else{
        return false;
    }
}

注意:此代码最初引用了Guava,但正如评论所指出的那样,此功能自JDK引入后,不再需要Guava。
如您所见,Guava/JDK版本更短,避免了多余的辅助对象。在equals的情况下,它甚至允许在早期的Object.equals()调用返回false时短路评估(公平地说:commons/lang有一个具有相同语义的ObjectUtils.equals(obj1,obj2)方法,可以用来代替EqualsBuilder以实现上述短路)。
因此:是的,commons lang构建器比手动构建的equals()和hashCode()方法(或Eclipse为您生成的可怕怪物)更可取,但Java 7+/Guava版本更好。
关于Hibernate的注释:
请注意,在equals()、hashCode()和toString()实现中使用延迟集合时要小心。如果没有打开的Session,这将失败。
注意(关于equals()):
a)在上述两个equals()版本中,您可能还想使用以下一种或两种快捷方式:
@Override
public boolean equals(final Object obj){
    if(obj == this) return true;  // test for reference equality
    if(obj == null) return false; // test for null
    // continue as above

b) 根据您对 equals() 合同的解释,您可能还需要更改该行或几行代码

    if(obj instanceof Bean){

to

    // make sure you run a null check before this
    if(obj.getClass() == getClass()){ 

如果您使用第二个版本,您可能还想在equals()方法内调用super(equals())。在这里,意见不一,该主题在以下问题中讨论:right way to incorporate superclass into a Guava Objects.hashcode() implementation?(虽然它是关于hashCode(),但同样适用于equals())。
注意(受kayahr评论启发) 如果您有许多原始字段,Objects.hashCode(..)(以及底层的Arrays.hashCode(...))可能表现不佳。在这种情况下,EqualsBuilder实际上可能是更好的解决方案。

35
Java 7中的Objects.equals也可以实现相同的功能:http://download.oracle.com/javase/7/docs/api/java/util/Objects.html#equals%28java.lang.Object,%20java.lang.Object%29 - Thomas Jung
3
如果我理解正确的话,Josh Bloch 在《Effective Java》一书中的第8条建议中提到,在编写 equals() 方法时应该避免使用 getClass() 方法,而应该使用 instanceof 进行判断。 - Jeff Olson
6
Guava不仅为可变参数创建一个数组对象,还将所有参数转换为对象。因此,当您向其传递10个int值时,您将得到10个Integer对象和一个数组对象。Commons-lang解决方案只创建单个对象,无论您附加多少值到哈希码中。在“equals”方法中也存在相同的问题。Guava将所有值都转换为对象,而commons-lang只创建一个新对象。 - kayahr
1
@wonhee 我强烈反对这种做法更好。使用反射来计算哈希码不是我会做的事情。性能开销可能可以忽略不计,但这样做感觉就是不对的。 - Sean Patrick Floyd
1
@kaushik 将一个类声明为 final 实际上解决了两个版本(instanceof 和 getClass())的潜在问题,只要你仅在叶子类中实现 equals() 方法。 - Sean Patrick Floyd
显示剩余18条评论

20

各位,醒醒吧!自从Java 7版本以来,标准库中就提供了用于equalshashCode的辅助方法。它们的使用方法完全等同于Guava方法。


a) 在提问时,Java 7 还不存在。 b) 从技术上讲,它们并不完全等价。JDK 有 Objects.equals 方法,而 Guava 则有 Objects.equal 方法。我只能使用 Guava 版本的静态导入。我知道这只是表面现象,但非 Guava 的代码显然更加混乱。 - Sean Patrick Floyd
你能举个例子,说明什么情况会导致陷入循环吗? - Mikhail Golubtsov
OP正在询问如何在Object中覆盖equals()方法。根据静态方法Objects.equals()的文档:“如果参数相等,则返回true,否则返回false。因此,如果两个参数都为null,则返回true,如果只有一个参数为null,则返回false。否则,使用第一个参数的equals方法来确定相等性。”因此,如果您在重写的实例equals()中使用了Objects.equals(),它将调用自己的equals方法,然后是Objects.equals(),然后再次是自己,导致堆栈溢出。 - dardo
@dardo 我们正在讨论实现结构相等性,这意味着如果两个对象的字段相同,则它们相互等同。请参考上面的Guava示例,了解如何实现equals方法。 - Mikhail Golubtsov
请仔细阅读最佳答案。在我的回答中,我提到了它(它提到了Guava,尽管问题是关于Apache Commons的)。我只是想说,已经有一种很好的方法可以使用标准库解决这个问题,而不需要使用这两个工具库之一。 - Mikhail Golubtsov
显示剩余2条评论

8

如果您不想依赖第三方库(也许您正在运行资源有限的设备),甚至不想自己编写方法,您也可以让 IDE 来完成这项工作,例如在 Eclipse 中使用:

Source -> Generate hashCode() and equals()...

您将获得“本地”代码,您可以根据自己的喜好进行配置,并且您必须在更改时提供支持。
示例(Eclipse Juno):
import java.util.Arrays;
import java.util.List;

public class FooBar {

    public String string;
    public List<String> stringList;
    public String[] stringArray;

    /* (non-Javadoc)
     * @see java.lang.Object#hashCode()
     */
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((string == null) ? 0 : string.hashCode());
        result = prime * result + Arrays.hashCode(stringArray);
        result = prime * result
                + ((stringList == null) ? 0 : stringList.hashCode());
        return result;
    }
    /* (non-Javadoc)
     * @see java.lang.Object#equals(java.lang.Object)
     */
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        FooBar other = (FooBar) obj;
        if (string == null) {
            if (other.string != null)
                return false;
        } else if (!string.equals(other.string))
            return false;
        if (!Arrays.equals(stringArray, other.stringArray))
            return false;
        if (stringList == null) {
            if (other.stringList != null)
                return false;
        } else if (!stringList.equals(other.stringList))
            return false;
        return true;
    }

}

15
没错,但是Eclipse生成的代码难以阅读和维护。 - Sean Patrick Floyd
7
请务必不要想象出由日食引发的可怕equals。如果您不想依赖第三方库,那么可以自己写一个像Objects.equal这样的一行代码方法。即使只使用一两次,它也能使代码更好! - maaartinus
1
@maaartinus Guava是一个第三方库。我指出,如果你想避免使用第三方库,我的解决方案可以使用。 - FrVaBe
1
@FrVaBe:我写道:“如果你不想依赖第三方库,那么就自己编写一个像Objects.equal这样的一行代码方法。”然后我写了一个一行代码的方法,你可以使用它来避免使用Guava,同时将equals方法的长度减少到大约一半。 - maaartinus
@maaartinus 好的,我明白你的意思了。我添加了一个示例,这样每个人都可以决定它是否太邪恶了。我真的不支持这种“帮助”,但至少它是有用的(两种方法都不像你想象的那么简单)。 - FrVaBe
显示剩余5条评论

7
EqualsBuilder和HashCodeBuilder与手写代码有两个主要区别:
  • 空值处理
  • 实例创建
EqualsBuilder和HashCodeBuilder使得比较可能为空的字段更加容易。手写代码会产生很多样板代码。
而EqualsBuilder则会为每个equals方法调用创建一个实例。如果你的equals方法经常被调用,这将会产生许多实例。
对于Hibernate,equals和hashCode实现没有任何区别。它们只是一些实现细节。对于几乎所有使用Hibernate加载的领域对象,(即使没有逃逸分析)可以忽略Builder的运行时开销。数据库和通信开销是显著的。
正如skaffman所提到的,反射版本不能在生产代码中使用。反射速度太慢,并且“实现”对于除了最简单的类之外的所有成员都不正确。考虑到新添加的成员将更改equals方法的行为,考虑所有成员也是很危险的。反射版本在测试代码中可能会有用。

我不同意反射实现“仅适用于最简单的类”的说法。使用构建器,您可以明确地排除字段,因此实现实际上取决于您的业务关键字定义。不幸的是,我不能否认基于反射的实现在性能方面存在问题。 - digitaljoel
1
@digitaljoel 是的,您可以排除字段,但这些定义不是重构保存的。所以我故意没有提到它们。 - Thomas Jung

4

0
在我看来,它与Hibernate不太兼容,特别是答案中比较某个实体的长度、名称和子项的示例。Hibernate建议在equals()和hashCode()中使用业务键使用业务键,他们有他们的理由。如果您在业务键上使用自动equals()和hashCode()生成器,那么没问题,只需要考虑之前提到的性能问题。但人们通常使用所有属性,这在我看来非常错误。例如,我目前正在使用带有@AutoProperty的Pojomatic编写实体的项目中,我认为这是一个真正糟糕的模式。
使用hashCode()和equals()的两种主要场景是:
  • 当您将持久类的实例放入Set中(表示多值关联的推荐方式)时
  • 当您使用分离实例的reattachment时
因此,让我们假设我们的实体看起来像这样:
class Entity {
  protected Long id;
  protected String someProp;
  public Entity(Long id, String someProp);
}

Entity entity1 = new Entity(1, "a");
Entity entity2 = new Entity(1, "b");

对于Hibernate来说,它们都是相同的实体,在某个时刻从某个会话中获取(它们的id和类/表相等)。但是当我们在所有属性上实现自动equals()和hashCode()时,我们得到了什么呢?

  1. 当您将实体2放入已存在实体1的持久集合中时,这将被重复放置,并且在提交期间会导致异常。
  2. 如果您想将分离的实体2附加到已存在实体1的会话中,则它们(可能,我没有特别测试过)不会被正确合并。

因此,对于99%的项目,我们使用以下在基本实体类中编写的equals()和hashCode()的实现,该实现与Hibernate概念一致:

@Override
public boolean equals(Object obj) {
    if (StringUtils.isEmpty(id))
        return super.equals(obj);

    return getClass().isInstance(obj) && id.equals(((IDomain) obj).getId());
}

@Override
public int hashCode() {
    return StringUtils.isEmpty(id)
        ? super.hashCode()
        : String.format("%s/%s", getClass().getSimpleName(), getId()).hashCode();
}

对于瞬态实体,我会执行与Hibernate在持久化步骤上相同的操作,即使用实例匹配。对于持久化对象,我会比较唯一键,即表/ID(我从不使用复合键)。

0

如果你只是处理实体bean,其中id是主键,你可以简化操作。

   @Override
   public boolean equals(Object other)
   {
      if (this == other) { return true; }
      if ((other == null) || (other.getClass() != this.getClass())) { return false; }

      EntityBean castOther = (EntityBean) other;
      return new EqualsBuilder().append(this.getId(), castOther.getId()).isEquals();
   }

-1

以防万一,其他人可能会发现这个有用,我为哈希码计算设计了这个辅助类,它避免了上述额外的对象创建开销(事实上,当你有继承时,Objects.hash()方法的开销甚至更大,因为它会在每个级别上创建一个新数组!)。

使用示例:

public int hashCode() {
    return HashCode.hash(HashCode.hash(timestampMillis), name, dateOfBirth); // timestampMillis is long
}

public int hashCode() {
    return HashCode.hash(super.hashCode(), occupation, children);
}

HashCode助手:

public class HashCode {

    public static int hash(Object o1, Object o2) {
        return add(Objects.hashCode(o1), o2);
    }

    public static int hash(Object o1, Object o2, Object o3) {
        return hash(Objects.hashCode(o1), o2, o3);
    }

    ...

    public static int hash(Object o1, Object o2, ..., Object o10) {
        return hash(Objects.hashCode(o1), o2, o3, ..., o10);
    }

    public static int hash(int initial, Object o1, Object o2) {
        return add(add(initial, o1), o2);
    }

    ...

    public static int hash(int initial, Object o1, Object o2, ... Object o10) {
        return add(... add(add(add(initial, o1), o2), o3) ..., o10);
    }

    public static int hash(long value) {
        return (int) (value ^ (value >>> 32));
    }

    public static int hash(int initial, long value) {
        return add(initial, hash(value));
    }

    private static int add(int accumulator, Object o) {
        return 31 * accumulator + Objects.hashCode(o);
    }
}

我已经发现,在一个领域模型中,10是最大合理的属性数量。如果你有更多的属性,你应该考虑重构并引入更多的类,而不是维护一堆字符串和基本类型。

缺点是:如果你主要有原始类型和/或需要深度哈希的数组,它就没有用了。(通常情况下,当你必须处理超出你控制范围的平面(传输)对象时,就会出现这种情况)。


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