整理Effective Java書中Item 10: Obey the general contract when overriding equals心得筆記

主旨

當覆寫 equals() 方法時,必須遵守其一般契約,這是設計 equals() 方法時的基本要求。如果不遵守這些規範,會導致難以預測的錯誤。

劃重點

equals() 方法的一般契約

  1. 對稱性:如果 a.equals(b) 返回 true,那麼 b.equals(a) 也應該返回 true
  2. 自反性:對於任何非 null 的參照變數 aa.equals(a) 必須返回 true
  3. 傳遞性:如果 a.equals(b) 返回 trueb.equals(c) 返回 true,那麼 a.equals(c) 必須返回 true
  4. 一致性:如果兩個物件相等,多次調用 a.equals(b) 應該返回相同的結果,前提是 b 沒有改變。
  5. null 的比較a.equals(null) 應該返回 false

為什麼要遵守這些規則?

  • 一致性和預測性:如果不遵守 equals() 的契約,可能會導致程序行為異常,進而破壞集合的行為,尤其是在使用如 HashSetHashMap 這類基於哈希值的容器時,會出現意料之外的結果。
  • 容器的正確性:不遵守契約可能會讓集合類型無法正確識別相等物件,從而造成錯誤的去重或插入。

實際範例:正確覆寫 equals() 方法

public class User {
    private String username;
    private int age;

    public User(String username, int age) {
        this.username = username;
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true; // 先檢查是否是同一個物件
        if (o == null || getClass() != o.getClass()) return false; // 處理null和不同類型的情況
        User user = (User) o;
        return age == user.age && username.equals(user.username); // 比對屬性
    }

    @Override
    public int hashCode() {
        return Objects.hash(username, age); // 確保hashCode與equals契約一致
    }
}

說明

在這個範例中,equals() 方法遵循了上述所有契約。使用 usernameage 作為兩個物件相等的標準,確保物件的相等性是基於其重要的屬性進行比較。

覆寫 hashCode() 方法是必要的,因為如果兩個物件相等(即 equals() 返回 true),它們的哈希值也必須相同。這是 HashSetHashMap 正確工作的基礎。若 hashCode() 方法與 equals() 方法不一致,可能會導致容器中的錯誤行為,如無法正確查找或去重相等的物件。

常見錯誤:不遵守 equals() 的契約

以下是一個錯誤範例,這種做法會導致無法正確識別相等的物件,尤其在容器中使用時會出現問題。

public class User {
    private String username;
    private int age;

    public User(String username, int age) {
        this.username = username;
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return username.equals(user.username); // 錯誤:沒有比對 age 屬性
    }

    @Override
    public int hashCode() {
        return Objects.hash(username); // 錯誤:hashCode 沒有考慮 age 屬性
    }
}

問題:

這個 equals() 方法沒有比對 age 屬性,導致相同 username 但不同 age 的兩個物件被認為是相等的。

同樣,hashCode() 也未能遵守契約,僅根據 username 計算哈希值,這將導致哈希衝突,影響如 HashSetHashMap 這類基於哈希值的容器的正確性。

小結

覆寫 equals() 方法時,一定要遵守其一般契約,確保方法正確運行,並避免容器中的錯誤行為。

同時,記得覆寫 hashCode() 方法,以確保它與 equals() 一致,保持 HashSetHashMap 的正確性。