HashCodeBuilder和EqualsBuilder的用法风格

35
我经常使用Apache的HashCodeBuilder和EqualsBuilder通过反射实现对象相等性,但最近一位同事告诉我,如果实体包含大量属性,则使用反射可能会导致性能损失巨大。为了避免使用错误的实现,我的问题是,你更喜欢以下哪种方法?为什么?
public class Admin {

    private Long id;
    private String userName;

    public String getUserName() {
        return userName;
    }

    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Admin)) {
            return false;
        }
        Admin otherAdmin  = (Admin) o;
        EqualsBuilder builder = new EqualsBuilder();
        builder.append(getUserName(), otherAdmin.getUserName());
        return builder.isEquals();
    }

    @Override
    public int hashCode() {
        HashCodeBuilder builder = new HashCodeBuilder();
        builder.append(getUserName());
        return builder.hashCode();
    }
}

对比。

public class Admin {

    private Long id;
    private String userName;

    public String getUserName() {
        return userName;
    }

    @Override
    public boolean equals(Object o) {
      return EqualsBuilder.reflectionEquals(this, o, Arrays.asList(id));
    }

    @Override
    public int hashCode() {
        return HashCodeBuilder.reflectionHashCode(this, Arrays.asList(id));
    }
}

你需要接受以下其中一个答案来适应你的问题。我也有同样的问题,无法确定哪个答案是正确的。请尝试接受任何一个答案。 - developer
3
为什么他需要接受下面的一个答案呢?如果问题没有得到满意的回答,为什么要接受呢?我认为应该保持开放状态,直到有人给出一个很好的答案。 - Churk
6个回答

24

当然,第二个选项更加优雅和简单。但是如果您关心性能,您应该选择第一种方法。如果安全管理器正在运行,则第二种方法也会失败。 如果我在您的情况下,我会选择第一种选项。

另外,在您的第一种方法中生成hashCode时存在错误: 应该使用builder.toHashCode()而不是builder.hashCode()。后者返回builder对象的哈希码。


2
根据HashCodeBuilder的更新实现,我可以使用hashCode()代替toHashCode(),因为hashCode()方法现在调用了toHashCode()。http://commons.apache.org/lang/api-2.6/index.html?org/apache/commons/lang/builder/HashCodeBuilder.html - tintin
6
有趣的事实:EqualsBuilder.equals()不会调用EqualsBuilder.isEquals()。这难道不是一个坏陷阱吗?! :-/ - Peter Wippermann
1
@PeterWippermann:不完全是这样 - EqualsBuilder.equals()EqualsBuilder.isEquals() 的签名完全不同,因此不容易混淆 - 您根本无法在EqualsBuilder.isEquals()的位置调用EqualsBuilder.equals()。对于 HashCodeBuilder.hashCode()HashCodeBuilder.toHashCode() 来说就不同了,方法签名完全相同。 - Gunter Ohrner

19

尽管第二个选项更加吸引人(因为只需要一行代码),但我会选择第一个选项。

原因很简单,就是性能。经过运行一个小测试后,我发现它们之间有非常大的时间差异。

为了大概了解时间,我创建了这两个简单的类:

package equalsbuildertest;

import java.math.BigDecimal;
import java.util.Date;

import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;

public class Class1 {

    private int field1;

    private boolean field2;

    private BigDecimal field3;

    private String field4;

    private Date field5;

    private long field6;

    public Class1(int field1, boolean field2, BigDecimal field3, String field4,
            Date field5, long field6) {
        super();
        this.field1 = field1;
        this.field2 = field2;
        this.field3 = field3;
        this.field4 = field4;
        this.field5 = field5;
        this.field6 = field6;
    }

    public Class1() {
        super();
    }

    public int getField1() {
        return field1;
    }

    public void setField1(int field1) {
        this.field1 = field1;
    }

    public boolean isField2() {
        return field2;
    }

    public void setField2(boolean field2) {
        this.field2 = field2;
    }

    public BigDecimal getField3() {
        return field3;
    }

    public void setField3(BigDecimal field3) {
        this.field3 = field3;
    }

    public String getField4() {
        return field4;
    }

    public void setField4(String field4) {
        this.field4 = field4;
    }

    public Date getField5() {
        return field5;
    }

    public void setField5(Date field5) {
        this.field5 = field5;
    }

    public long getField6() {
        return field6;
    }

    public void setField6(long field6) {
        this.field6 = field6;
    }

    @Override
    public boolean equals(Object o) {
      return EqualsBuilder.reflectionEquals(this, o);
    }

    @Override
    public int hashCode() {
        return HashCodeBuilder.reflectionHashCode(this);
    }

}

与:

package equalsbuildertest;

import java.math.BigDecimal;
import java.util.Date;

import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;

public class Class2 {

    private int field1;

    private boolean field2;

    private BigDecimal field3;

    private String field4;

    private Date field5;

    private long field6;

    public Class2(int field1, boolean field2, BigDecimal field3, String field4,
            Date field5, long field6) {
        super();
        this.field1 = field1;
        this.field2 = field2;
        this.field3 = field3;
        this.field4 = field4;
        this.field5 = field5;
        this.field6 = field6;
    }

    public Class2() {
        super();
    }

    public int getField1() {
        return field1;
    }

    public void setField1(int field1) {
        this.field1 = field1;
    }

    public boolean isField2() {
        return field2;
    }

    public void setField2(boolean field2) {
        this.field2 = field2;
    }

    public BigDecimal getField3() {
        return field3;
    }

    public void setField3(BigDecimal field3) {
        this.field3 = field3;
    }

    public String getField4() {
        return field4;
    }

    public void setField4(String field4) {
        this.field4 = field4;
    }

    public Date getField5() {
        return field5;
    }

    public void setField5(Date field5) {
        this.field5 = field5;
    }

    public long getField6() {
        return field6;
    }

    public void setField6(long field6) {
        this.field6 = field6;
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Class2)) {
            return false;
        }
        Class2 other = (Class2) obj;
        EqualsBuilder builder = new EqualsBuilder();
        builder.append(field1, other.field1);
        builder.append(field2, other.field2);
        builder.append(field3, other.field3);
        builder.append(field4, other.field4);
        builder.append(field5, other.field5);
        builder.append(field6, other.field6);
        return builder.isEquals();
    }

    @Override
    public int hashCode() {
        HashCodeBuilder builder = new HashCodeBuilder();
        builder.append(getField1());
        builder.append(isField2());
        builder.append(getField3());
        builder.append(getField4());
        builder.append(getField5());
        builder.append(getField6());
        return builder.hashCode();

    };

}

第二个类与第一个类基本相同,只是具有不同的equals和hashCode方法。

之后,我创建了以下测试:

package equalsbuildertest;

import static org.junit.Assert.*;

import java.math.BigDecimal;
import java.util.Date;

import org.junit.Test;

public class EqualsBuilderTest {

    @Test
    public void test1() {
        Class1 class1a = new Class1(1, true, new BigDecimal(0), "String", new Date(), 1L);
        Class1 class1b = new Class1(1, true, new BigDecimal(0), "String", new Date(), 1L);
        for (int i = 0; i < 1000000; i++) {
            assertEquals(class1a, class1b);
        }
    }

    @Test
    public void test2() {
        Class2 class2a = new Class2(1, true, new BigDecimal(0), "String", new Date(), 1L);
        Class2 class2b = new Class2(1, true, new BigDecimal(0), "String", new Date(), 1L);
        for (int i = 0; i < 1000000; i++) {
            assertEquals(class2a, class2b);
        }
    }

}

这些测试非常简单,只是用来测量时间。

结果如下:

  • test1 (2,024 s)
  • test2 (0.039 s)

我选择它们完全相等以获得最长的时间。如果您选择使用NotEquals条件进行测试,您将会有更短的时间,但也会保持非常大的时间差异。

我在一台64位Intel Core i5-3317U CPU @1.70GHz x4的计算机上运行这些测试,使用Fedora 21和Eclipse Luna。

总之,为了节省几行代码(您可能无法使用模板在Eclipse中找到,位于Windows ->首选项中的Java ->编辑器 ->模板),我不会冒险承担如此大的性能差异。

${:import(org.apache.commons.lang3.builder.HashCodeBuilder, org.apache.commons.lang3.builder.EqualsBuilder)}
@Override
public int hashCode() {
    HashCodeBuilder hashCodeBuilder = new HashCodeBuilder();
    hashCodeBuilder.append(${field1:field});
    hashCodeBuilder.append(${field2:field});
    hashCodeBuilder.append(${field3:field});
    hashCodeBuilder.append(${field4:field});
    hashCodeBuilder.append(${field5:field});
    return hashCodeBuilder.toHashCode();
}

@Override
public boolean equals(Object obj) {
    if (this == obj) {
        return true;
    }
    if (obj == null) {
        return false;
    }
    if (getClass() != obj.getClass()) {
        return false;
    }
    ${enclosing_type} rhs = (${enclosing_type}) obj;
    EqualsBuilder equalsBuilder = new EqualsBuilder();
    equalsBuilder.append(${field1}, rhs.${field1});
    equalsBuilder.append(${field2}, rhs.${field2});
    equalsBuilder.append(${field3}, rhs.${field3});
    equalsBuilder.append(${field4}, rhs.${field4});
    equalsBuilder.append(${field5}, rhs.${field5});${cursor}
    return equalsBuilder.isEquals();
}

11

我希望选择第二个选项,理由如下:

  1. 显然它更容易阅读

  2. 对于第一个选项的性能论证,除非其中包含相关度量标准,否则我不会购买。例如,基于反射的"equals"会为典型的端到端请求延迟增加多少毫秒?总体上会增加多少百分比? 没有了解这些,优化很可能是过早的。


3

你的问题清晰地说明了第二种方法的一个好处:

在第一种情况下,很容易犯错,错误的写法是 return builder.hashCode(),而正确的写法是 return builder.toHashCode(),这会导致微妙的错误,很难追踪。

第二种情况消除了这种笔误的可能性,减少了在键盘上敲打头部以查找错误的次数。


2
这段评论针对Apache Commons v2.5或更新版本是不正确的,Apache Commons v2.5已于2010年2月发布。引用javadoc的话:"由于误调用toHashCode()的Bugs的可能性及其本身的hashCode的不太重要性,因此返回从toHashCode()计算出来的hashCode"。 javadoc在此处可用:http://commons.apache.org/proper/commons-lang/release-history.html。 - cfogelberg
很高兴知道这个漏洞已经修复了。但对于那些仍然使用旧版本库的人来说,这个答案仍然是有参考价值的(是的,它已经几年了,人们“应该”更新,但还有很多“企业级”的地方在这方面行动缓慢)。 - Krease

3
我认为这两种实现都不是很好。我会认为EqualsBuilder不适合使用,原因如下:
  1. 不可扩展。如果你要断言的某个字段应该将null和空字符串视为相等怎么办?
  2. 你必须像硬编码变量一样维护列表。也就是说,你必须列出所有要比较的变量。此时,a == o.getA() && b == o.getB() ...没有任何区别。
  3. 使用反射会占用额外的资源,正如你所指出的,在一个处理数十亿个对象的企业应用程序中,使用反射进行equals操作就像有一个内存泄漏一样糟糕。
我认为需要比Apache更好的框架。

确保属性在设置器中是NULL或空字符串。问题解决了。 - Lluis Martinez

2
同意@Churk的观点,Apache HashCodeBuilder和EqualsBuilder实现得不好。HashCodeBuilder仍在使用质数!此外,它做了很多不必要的工作。你读过源代码吗?
自Java 5以来(如果不是更早),AbstractHashMap<>已经没有使用质数取模来定位哈希桶。相反,桶的数量是2的幂,并且使用哈希码的低阶N位来定位桶。
此外,它会“混合”应用程序提供的哈希码,使位均匀分布,因此桶被均匀填充。
因此,覆盖int Object.hashCode()的正确方法是返回在使用该类的任何集合中共同居住的对象群体中具有最高元数的最简单、常量值。通常,ID值不变是您最好的选择。如果您的ID字段是整数,请将其转换为(int)并返回它。如果它是一个字符串或其他对象,请返回它的哈希码。你明白了。对于复合标识符,返回具有最不同值的字段(或其哈希码)。越少越好。
当然,hashCode()和equals()之间的协议必须得到遵守。因此,equals()应相应地实现。hashCode()不需要使用完全符合等式所需的限定符,但是在hashCode()中使用的任何字段都必须在equals()中使用。这里,像StringUtils.equals(s1, s2)这样的方法对于一致且安全地处理空值非常有用。

更重要的是:HashCodeBuilder.reflectionHashCode()和EqualsBuilder.reflectionEquals()实际上有些危险,因为它们很容易违反equals()/hashCode()合同中的“一致”部分。这里发生了什么:如果一个对象被放置在哈希表/集合中,并且随后在hashCode()或equals()中使用的字段被更改,则该对象在集合中实际上是孤立的,无法访问。只有不变的标识符字段应该在hashCode()或equals()中使用。开发人员不能将此选择留给通用的“builder”。 - Charlie
将对象插入集合后更改字段对于任何equals/hash实现都是一个问题。 - Bax

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