CodeGym /課程 /JAVA 25 SELF /equals 與 hashCode 的契約

equals 與 hashCode 的契約

JAVA 25 SELF
等級 29 , 課堂 0
開放

1. 介紹

我們如何比較物件

在 Java 中,物件不只是資料,每個物件在記憶體中都有自己的位址。運算子 == 回答的是「是同一個盒子嗎?」也就是比較參考(位址),而不是內容。方法 equals 則用來比較內容本身。預設情況下,如果沒有覆寫,equals 的行為與 == 相同。

Person p1 = new Person("Ivan", 20);
Person p2 = new Person("Ivan", 20);

System.out.println(p1 == p2); // false — 這是記憶體中的不同物件!

若希望兩個不同的物件在資料上被視為相等(例如所有關鍵欄位都相同),必須覆寫 equals。而且如果你計畫在雜湊型集合中使用這些物件,務必同時正確覆寫 hashCode

class Person {
    String name;
    int age;

    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true; // 這是同一個物件
        if (o == null || getClass() != o.getClass()) return false; // 檢查類別
        Person person = (Person) o; // 轉型為需要的型別
        return age == person.age && name.equals(person.name); // 比較欄位
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age); // 使其能與 HashSet/HashMap 正常運作
    }
}

如此的覆寫對於正確使用 HashSet/HashMap 至關重要:沒有 equalshashCode,集合會把即使資料相同的物件也視為不同。

equals 中的等價關係取決於你的需求:可以依所有欄位比較,也可以只比較部分欄位(例如 User 的 email)——關鍵是保持一致並遵守契約。

在哪些地方特別重要?

  • 在以雜湊表為基礎的集合:HashSetHashMapLinkedHashSet 等。
  • 在集合中的查找與刪除:若沒有正確的 equals,目標物件可能「找不到」。
  • 在商務邏輯中:例如兩個 User 的 email 相同時,應被視為同一名使用者。

hashCode 有什麼用?

雜湊型集合(例如 HashSetHashMap)使用雜湊表。方法 hashCode 會計算一個整數——物件要放入的「桶位地址」。如果兩個物件依 equals 相等,它們的 hashCode 必須相同。若違反此規則,集合行為將變得不可預期。

2. equals 與 hashCode 的契約

equals 契約

equals 的基本行為要求:

  • 自反性: a.equals(a) 總是 true。
  • 對稱性: 如果 a.equals(b) 為 true,則 b.equals(a) 也為 true。
  • 傳遞性: 如果 a.equals(b)b.equals(c),那麼 a.equals(c) 也為 true。
  • 一致性: 在物件未變更時,呼叫結果應保持穩定。
  • 與 null 的比較: 任何物件都不等於 null

hashCode 契約

  • 如果兩個物件依 equals 相等,它們的 hashCode 必須相等。
  • 如果物件不相等,它們的雜湊碼可能相同(允許碰撞,但不理想)。
  • 只要物件的邏輯狀態未改變,其 hashCode 應保持不變。

換言之,相同的 hashCode 是必要但非充分條件:相同的雜湊碼不保證依 equals 相等。

3. equals 與 hashCode 的實作:範例

來看 Person 類別,將相等性定義為 nameage 欄位相同。

public class Person {
    private String name;
    private int age;

    // 建構子、getter、setter...

    @Override
    public boolean equals(Object o) {
        if (this == o) return true; // 比較參考
        if (o == null || getClass() != o.getClass()) return false; // 檢查類別

        Person person = (Person) o; // 轉型

        // 比較欄位
        return age == person.age &&
               (name != null ? name.equals(person.name) : person.name == null);
    }

    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age; // 31 是常見的質數選擇
        return result;
    }
}
  • 先做快速檢查:參考與類別。
  • 接著比較關鍵欄位。
  • hashCode 中使用質數 31 以降低碰撞。

使用 Objects.equals 與 Objects.hash

自 Java 7 起,Objects 類別可簡化程式並讓它對 null 更安全:

import java.util.Objects;

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Person person = (Person) o;
    return age == person.age &&
           Objects.equals(name, person.name);
}

@Override
public int hashCode() {
    return Objects.hash(name, age);
}

4. equals、hashCode 與 compareTo:如何關聯

equals 與 compareTo 如何關聯?

介面 Comparable 定義了方法 compareTo,對「較小/相等/較大」分別回傳負值/零/正值。理想情況下,由 a.compareTo(b) == 0 應可推得 a.equals(b)。反之則非必然。

若兩者不同步(例如,compareTo 只依年齡比較,而 equals 同時依姓名與年齡),則可排序的集合如 TreeSet/TreeMap 可能會出現意外行為:就順序而言被視為「相等」,但內容上並不相等。

集合中的 equals 與 hashCode

  • HashSetHashMap 中,新增/查找/刪除操作都仰賴正確實作 equalshashCode
  • 若未覆寫,這些集合會將物件視為「不同」,即使其資料相同。

5. 範例:在集合中的運作方式

HashSet:儲存唯一的物件

Set<Person> people = new HashSet<>();
people.add(new Person("Ivan", 20));
people.add(new Person("Ivan", 20)); // 重複

System.out.println(people.size()); // 若 equals/hashCode 實作正確,結果為 1

若沒有正確的 equals/hashCode,集合中會出現兩個物件。

HashMap:依鍵查找

Map<Person, String> map = new HashMap<>();
Person p1 = new Person("Anna", 25);
Person p2 = new Person("Anna", 25);

map.put(p1, "使用者 1");
System.out.println(map.get(p2)); // "使用者 1",如果 equals/hashCode 實作正確

若未遵守契約,集合會回傳 null——對它而言這是「不同」的鍵。

6. Best practices:實作建議

  • equals/hashCode 中納入所有決定物件「身分」的欄位。
  • 計算 hashCode 時避免使用在加入集合後可能會變動的欄位。
  • 讓 IDE 產生方法以降低手誤風險。
  • equals 中先檢查 this == o,再檢查類別,最後比較欄位。
  • 比較物件欄位時使用 Objects.equals
  • 計算雜湊碼時使用 Objects.hash 或常見的 31 乘法樣板。

7. 實用細節

為什麼不能只用 hashCode?

碰撞無可避免:不同物件可能有相同的 hashCode。雜湊只是快速定位桶位;最後的相等判定由 equals 決定。

可以不覆寫 equals 與 hashCode 嗎?

只有在你確定物件永遠不會按內容比較,且不會作為集合中的鍵或唯一元素時才行。在實務上這種情況很少見。

==、equals 與 compareTo 的差異

運算子/方法 比較什麼? 用途是什麼?
==
參考(記憶體位址) 檢查「是否同一個物件?」
equals
物件內容 依商務邏輯判定相等
compareTo
順序(較小/相等/較大) 排序、建立順序

8. 實作 equals 與 hashCode 的常見錯誤

錯誤 1:只覆寫了 equals,卻忘了 hashCode。 物件被視為相等,但落在雜湊表的不同桶位——查找與刪除會失效。

錯誤 2:在 hashCode 中使用可變欄位。 若欄位在加入集合後改變,物件會「迷路」:雜湊碼變了,但桶位不會自動更新。

錯誤 3:違反 equals 的對稱性/傳遞性。 a.equals(b) 為 true,而 b.equals(a) 為 false,或傳遞性被破壞——集合會出現不可預期的行為。

錯誤 4:在 equals 中未檢查類別。 比較不同類別的物件會導致錯誤結果或丟出例外。

錯誤 5:用 == 比較字串與物件。運算子 == 比較的是參考;請使用 equals 比較內容。

錯誤 6:compareTo 與 equals 不一致。 如果 a.compareTo(b) == 0,但 !a.equals(b)TreeSet/TreeMap 可能會把元素視為在順序上相等、但在相等性上不同——這會造成「幽靈」元素與重複項目。

1
任務
JAVA 25 SELF, 等級 29, 課堂 0
上鎖
虛擬世界中的唯一城市記錄 🗺️
虛擬世界中的唯一城市記錄 🗺️
1
任務
JAVA 25 SELF, 等級 29, 課堂 0
上鎖
在全新人資系統中的員工管理 👥
在全新人資系統中的員工管理 👥
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION