DDD實體值物件的equals和hashcode方法實現 - wimdeblauwe

發表於2021-04-27

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,...),那麼你可能不希望覆蓋的方法。

相關文章