DDD实体值对象的equals和hashcode方法实现 - wimdeblauwe


Java中的所有类均继承自java.lang.Object,它有equals()hashCode()方法,这两个方法是你定义自己的类时通常应该重写两个重要方法。
equals()比较两个个对象以检查它们是否代表同一对象是很重要的;如果将对象放在HashMap或HashSet中,hashCode()则很重要。它提供了那些数据结构所使用的哈希值。
即使您不了解领域驱动设计,您也可能听说过实体和值对象。如果您还没有,请简要回顾一下它们之间的区别:

  • 实体:在应用程序域中具有唯一标识的对象。例如,User或Invoice。
  • 值对象:仅因它们表示的值而重要的对象。例如,一个Money或Temperature对象。通常,这些对象是不可变的。

 
值对象的equals和hashcode方法
public class Temperature {
    private final double value;
    private final Unit unit;

    public Temperature(double value,
                       Unit unit) {
        this.value = value;
        this.unit = unit;
    }

    public double getValue() {
        return value;
    }

    public Unit getUnit() {
        return unit;
    }

    enum Unit {
        KELVIN, CELCIUS, FAHRENHEIT;
    }
}

对于值对象,我们想说明所有属性相等时对象是相等的。equals()实现应该是这样的:
public class Temperature {

    ...

    @Override
    public boolean equals(Object o) {
        if (this == o) { 
            return true;
        }
        if (o == null || getClass() != o.getClass()) { 
            return false;
        }
        Temperature that = (Temperature) o; 
        return Double.compare(that.value, value) == 0 && unit == that.unit; 
    }

    @Override
    public int hashCode() {
        return Objects.hash(value, unit); 
    }

    ...
}

  • 如果传入的对象与当前对象是相同的引用(在内存中),则相同。
  • 一个对象永远不能等于null也不能等于另一个类的对象。
  • 我们可以安全地转换传入的对象,因为我们确定它与该对象属于同一类。
  • 比较传入对象和当前对象的每个属性
  • 使用JDKObjects.hash()方法使用当前对象的所有属性生成哈希码。

现在我们可以验证Temperature具有相同属性的2个对象是否相等:
@Test
void testEqualTemperature() {
    Temperature temperature1 = new Temperature(37.0, Temperature.Unit.CELCIUS);
    Temperature temperature2 = new Temperature(37.0, Temperature.Unit.CELCIUS);

    boolean equal = temperature1.equals(temperature2);
    assertTrue(equal);
}

测试hashCode()实现:
@Test
void testHashCodeForEqualObjects() {
    Temperature temperature1 = new Temperature(37.0, Temperature.Unit.CELCIUS);
    Temperature temperature2 = new Temperature(37.0, Temperature.Unit.CELCIUS);

    int hashCode1 = temperature1.hashCode();
    int hashCode2 = temperature2.hashCode();

    assertThat(hashCode1).isEqualTo(hashCode2);
}

 
实体的equals和hashcode方法
对于实体而言,真正重要的是标识符。我们希望看到2个实例具有与同一事物相同的标识符,即使其他属性不同也是如此。假设这个简单的User实体:
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class User {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    protected User() {
    }

    public User(String name) {
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

由于我们只关心该id领域,因此一个简单的实现看起来像这样:

 @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        User user = (User) o;
        return Objects.equals(id, user.id);
    }

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

不幸的是,这是错误的。问题在于该id字段是由数据库生成的,并且仅在对象持久化之后才填写。因此,对于同一个对象,id最初是null,然后在将其存储在数据库中之后获得某个值。
幸运的是,Vlad Mihalcea向我们展示了如何正确实现这一点
 

  @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        User user = (User) o;
        return id != null &&
                id.equals(user.id);
    }

    @Override
    public int hashCode() {
        return getClass().hashCode();
    }

两个重要注意事项:
  • 如果id填充,我们会看到User等同的的情况,如果在两个User实例没有被存储在数据库中,则永远不会相等。
  • Hashode使用硬编码的值,因为在创建对象的时间和将其保留在数据库中的时间之间不允许hashCode值发生变化。

请参阅如何使用JPA实体标识符(主键)实现equals和hashCode以获得有关此内容的更详细信息。
 
数据库主键生成的实体的两个方法
如果你不喜欢,上面JPA实体实现equals()和hashCode()的方式,那么就可以采取不同的路线。在创建对象之前生成主键时,有两个优点:

  1. id可在构造函数中赋值,你不能创建“无效”对象。
  2. equals()和hashCode()方法可以简化为仅考虑id。

import org.springframework.util.Assert;

import javax.persistence.Entity;
import javax.persistence.Id;

@Entity
public class Book {
    @Id
    private Long id;

    private String name;

    protected Book() {
    }

    public Book(Long id,
                String name) {
        Assert.notNull(id, "id should not be null");
        Assert.notNull(name,
"name should ot be null");
        this.id = id;
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

Book实体没有@GeneratedValue注释,因此我们将需要在构造时传递一个值。
现在我们知道该id字段永远不会null,我们可以使用以下实现:
 
@Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        Book book = (Book) o;
        return id.equals(book.id);
    }

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

测试代码:
 @Test
    void testEquals() {
        Book book1 = new Book(1L, "Taming Thymeleaf");
        Book book2 = new Book(1L,
"Taming Thymeleaf");

        assertThat(book1).isEqualTo(book2);
    }
  @Test
    void testEquals() {
        Book book1 = new Book(1L,
"Taming Thymeleaf");
        Book book2 = new Book(1L,
"Totally different title");

        assertThat(book1).isEqualTo(book2);
    }

 
测试equals和hashCode实现
为确保正确实现您的方法,请使用EqualsVerifier库包。
将其添加到您的pom.xml:

<dependency>
    <groupId>nl.jqno.equalsverifier</groupId>
    <artifactId>equalsverifier</artifactId>
    <version>3.6</version>
    <scope>test</scope>
</dependency>

并编写测试:
  @Test
    public void equalsContract() {
        EqualsVerifier.forClass(Temperature.class).verify();
    }

这将测试是否equals()是自反的,对称的,可传递的和一致的。它还会测试是否hashCode()遵守java.lang.ObjectAPI中定义的合同。
 
结论
要正确实现equals()and hashCode(),首先确定您的对象是值对象还是实体很重要。如果是其中之一,则可以遵循博客中列出的规则。如果两者都不是(例如Controller,Service,Repository,...),那么你可能不希望覆盖的方法。