33

我经常使用 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));
    }
}
4

6 回答 6

22

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

在生成 hashCode 的第一种方法中也有一个错误
它应该builder.toHashCode() 代替builder.hashCode(). 后者返回哈希码构建器对象的哈希码。

于 2012-06-06T11:39:58.370 回答
17

即使第二个选项更有吸引力(因为它只是一行代码)我会选择第一个选项。

原因只是性能。在运行了一个小测试后,我发现它们之间的时间差非常大。

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

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);
        }
    }

}

该测试非常简单,仅用于测量时间。

结果如下:

  • 测试1(2,024 秒)
  • 测试2(0,039 秒)

我选择他们完全平等是为了拥有最伟大的时代。如果您选择使用 NotEquals 条件进行测试,您将有更短的时间,但也会保持很大的时间差。

我在带有 Fedora 21 和 Eclipse Luna 的 64 位 Intel Core i5-3317U CPU @1.70GHz x4 上运行此测试。

总之,我不会冒如此大的性能差异来保存几行您可能无法使用模板输入的代码(在 Windows 下的 Eclipse -> Preferences 在 Java -> Editor -> Templates 中找到) 比如这样:

${: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();
}
于 2015-04-07T20:03:42.670 回答
11

我更喜欢第二种选择,原因有两个:

  1. 显然更容易阅读

  2. 我不会为第一个选项购买性能参数,除非这些参数包含相关指标。例如,基于反射的“等于”会增加多少毫秒到典型的端到端请求延迟?总体而言,那会增加多少%?不知道优化是为时过早的好机会

于 2013-11-26T18:02:15.500 回答
3

我想说这些都不是很好的实现。我认为 EqualsBuilder 不是一个很好的框架,原因如下:

  1. 不可扩展。如果您尝试断言相等的字段之一应该将空值和空白视为相等怎么办?
  2. 您必须维护变量列表,就好像它们是硬编码变量一样。这意味着您必须列出要比较的所有变量。此时 a == o.getA() && b == o.getB() 之间没有任何不同 ...
  3. 正如您所指出的,使用反射会占用额外的资源,并且在粉碎数十亿对象的企业应用程序中。进行这种反射等于与内存泄漏一样糟糕。

我会说需要一个比 Apache 更好的框架。

于 2015-02-16T00:51:19.380 回答
3

您所写的问题清楚地说明了第二种方法的好处之一:

在第一种情况下,容易犯错误return builder.hashCode(),而不是正确的return builder.toHashCode(),这将导致难以追踪的细微错误。

第二种情况消除了这种错字的可能性,从而减少了试图找到错误的头撞键盘。

于 2014-11-28T02:51:35.007 回答
2

同意@Churk,Apache HashCodeBuilder 和 EqualsBuilder 没有很好地实现。HashCodeBuilder 仍在玩质数!此外,它还做了很多不必要的工作。你读过源码吗?

从 Java 5(如果不是更早)开始,AbstractHashMap<> 没有使用素数的模来定位哈希桶。相反,桶的数量是 2 的幂,并且哈希码的低 N 位用于定位桶。

此外,它将“混合”应用程序提供的哈希码,以便比特均匀分布,从而均匀地填充桶。

因此,重写 int Object.hashCode() 的正确方法是返回最简单的常量值,该值在使用该类 的任何集合中共存的对象中具有最高的数量。

通常,未修改的 ID 值是您的最佳选择。如果您的 ID 字段是完整的,只需将其转换为 (int) 并返回即可。如果是 String 或其他 Object,则只返回其哈希码。你明白了。对于复合标识符,返回具有最不同值的字段(或其 hashCode)。少即是多。

当然,必须满足 hashCode() 和 equals() 之间的约定。因此,应该相应地实现equals()。hashCode() 不需要使用相等所需的完整限定符,但 hashCode() 中使用的任何字段都必须在 equals() 中使用。在这里,像 StringUtils.equals(s1, s2) 这样的方法对于一致且安全地处理空值很有用。

于 2015-07-16T15:10:26.500 回答