整理Effective Java書中Item 11: Always override hashCode when you override equals 心得筆記

主旨

當覆寫 equals() 方法時,必須同時覆寫 hashCode()。如果不這麼做,將違反 hashCode 的契約,並且可能會在 HashMapHashSet 等集合中出現異常行為。

劃重點

hashCode 方法的契約

  1. 一致性:在同一個應用程式執行期間,多次調用同一物件的 hashCode() 方法,若該物件的 equals() 比較資訊未修改,則應該返回相同的結果。
  2. 相等的物件具有相等的哈希碼:若兩個物件根據 equals() 方法比較為相等,則兩者的 hashCode() 必須相同。
  3. 不相等的物件不必具有不同的哈希碼:如果兩個物件不相等,則不要求它們的 hashCode() 必須不同。不過,為不相等的物件產生不同的哈希碼有助於提高散列表的效能。

為何要遵守這些規範?

  • 避免錯誤:若沒有覆寫 hashCode(),即使兩個物件在 equals() 方法中被判定為相等,它們的哈希碼也會不同,這會破壞 HashMapHashSet 等基於哈希表的集合的運作,可能導致查詢失敗,或是發生錯誤的去重操作。

正確覆寫 hashCode() 方法的範例:

public class PhoneNumber {
    private short areaCode;
    private short prefix;
    private short lineNum;

    @Override
    public int hashCode() {
        int result = Short.hashCode(areaCode);
        result = 31 * result + Short.hashCode(prefix);
        result = 31 * result + Short.hashCode(lineNum);
        return result;
    }
}

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

以下是一個錯誤範例,這會導致兩個邏輯上相等的物件在 HashMap 中無法正確操作:

Map<PhoneNumber, String> m = new HashMap<>();
m.put(new PhoneNumber(707, 867, 5309), "Jenny");
System.out.println(m.get(new PhoneNumber(707, 867, 5309))); // null

問題:

當你使用 PhoneNumber 類的實例作為 HashMap 的鍵時,未覆寫 hashCode() 會導致兩個邏輯上相等的 PhoneNumber 實例擁有不同的哈希碼,從而導致 get 方法無法正確返回已插入的值。

此時,你可能會期望 m.get(new PhoneNumber(707, 867, 5309)) 返回 “Jenny”,但實際上,它返回 null。注意,這裡涉及到兩個 PhoneNumber 實例:一個用來插入到 HashMap 中,另一個相等的實例用來進行(嘗試)檢索。由於 PhoneNumber 類未覆寫 hashCode() 方法,這會導致兩個相等的實例具有不相等的哈希碼,違反了 hashCode 方法的契約。

因此,get 方法查找電話號碼時,可能會在與 put 方法存儲電話號碼時不同的哈希桶中查找,即使這兩個實例可能碰巧分配在同一個哈希桶中,get 方法也幾乎肯定會返回 null。這是因為 HashMap 優化了每個條目的哈希碼並進行了快取,如果哈希碼不匹配,它不會檢查物件是否相等。

小結

  • 覆寫 equals() 時,一定要覆寫 hashCode(),這樣才能確保 HashMapHashSet 等基於哈希表的集合正確運行。
  • hashCode() 方法需要遵守其契約,並確保邏輯相等的物件擁有相同的哈希碼。
  • 在設計時,避免使用簡單的、會使所有物件具有相同哈希碼的方法,這樣會降低散列表的效能,導致最終退化為鏈表查找。