每個 Java 物件都有 hashCode() 與 equals() 方法。許多類覆寫這些方法的預設實作,以在物件實例間提供更深層語義可比性。本文介紹創建 Java 類有效準確定義 hashCode() 與 equals() 時應遵循的規則與指南。
Object 類有兩種方法推斷物件身份:equals() 與 hashCode()。一般來說,若覆寫其中一個,必須同時覆寫兩者,因兩者間有必須維持的關鍵關係:根據 equals(),若兩物件相等,它們必須有相同的 hashCode() 值(雖反向通常不成立)。
特定類 equals() 語義由實作者定義;定義對特定類而言 equals() 意味著什麼是設計工作一部分。Object 提供的預設實作簡單引用等式:
public boolean equals(Object obj) { return (this == obj); }
此預設下,只有引用真正同一物件時兩引用才相等。同樣,Object 提供的 hashCode() 預設實作透過將物件記憶體位址映射為整數值生成。因某些架構位址空間大於 int 範圍,兩不同物件有相同 hashCode() 可能。若覆寫 hashCode(),仍可用 System.identityHashCode() 取得此類預設值。
預設基於身份的 equals()、hashCode() 實作合理,但某些類希望放寬等式定義。例如 Integer 類定義 equals() 類似:
public boolean equals(Object obj) {
return (obj instanceof Integer
&& intvalue() == ((Integer) obj).intvalue());
}
此定義下,僅當包含相同整數值時兩 Integer 物件相等。結合不可變的 Integer,使 Integer 作為 HashMap 鍵值可行。此基於值的相等法可用於 Java 類庫中所有基本型別包裝類(Integer、Float、Character、Boolean)與 String(若兩 String 含相同順序字符則相等)。因這些類不可變且實作 hashCode()、equals(),它們都是良好的雜湊鍵值。
若 Integer 不覆寫 equals()、hashCode() 會怎樣?若從未在 HashMap 或其他基於雜湊的集合用 Integer 做鍵值,什麼也不會發生。但若在 HashMap 用這類 Integer 做鍵值,除非在 get() 調用中用與 put() 極其相似的 Integer 實例,否則無法可靠檢索相關值。這要求確保整個程式中,只能用對應特定整數值的 Integer 物件一個實例。不言而喻,此法極不方便且錯誤頻傳。
Object 的介面契約要求:若根據 equals() 兩物件相等,則必須有相同 hashCode() 值。當識別能力整個包含在 equals() 中時,為何根物件類需要 hashCode()?hashCode() 純粹為效率。Java 平台設計者預料到典型 Java 應用中基於雜湊的集合類(Hashtable、HashMap、HashSet)的重要性——用 equals() 與許多物件比較計算上非常昂貴。使所有 Java 物件支援 hashCode() 並結合基於雜湊的集合,可實現有效儲存與檢索。
實作 equals()、hashCode() 有限制,Object 文件列舉。特別是 equals() 必須顯示以下屬性:
a、b,a.equals(b) 當且僅當 b.equals(a)a,a.equals(a)a.equals(b) 且 b.equals(c),則 a.equals(c)hashCode() 值Object 規範未明確要求 equals()、hashCode() 必須一致——它們結果在後續調用中將相同,假設「不改變物件相等性比較中使用的任何資訊」。這聽起來像「計算結果不變,除非實際情況如此」。此模糊陳述通常解釋為相等性與雜湊值計算應是物件可確定功能,而非其他。
人們易滿足 Object 類規範對 equals()、hashCode() 要求。決定是否、如何覆寫 equals() 除判斷外還要求其他。在簡單不可變值類中,如 Integer(事實上幾乎所有不可變類),選擇明顯——相等性應基於基本物件狀態相等性。Integer 情況下,物件唯一狀態是基本整數值。
對可變物件,答案不總是清楚。equals()、hashCode() 是否應基於物件身份(如預設實作)或物件狀態(如 Integer、String)?無簡單答案——取決於類的計劃用法。對 List、Map 等容器,眾說紛紜。Java 類庫多數類,含容器類,錯誤在於根據物件狀態提供 equals()、hashCode() 實作。
若物件 hashCode() 值可基於狀態更改,則當用此類物件作基於雜湊集合鍵值時必須注意,確保當它們用作雜湊鍵值時不允許更改狀態。所有基於雜湊集合假設:當物件雜湊值用作集合鍵值時不會改變。若鍵值在集合中時其雜湊碼被更改,將產生不可預測且易混淆結果。實踐中通常非問題——不常用 List 等可變物件做 HashMap 鍵值。
簡單可變類範例是 Point,根據狀態定義 equals()、hashCode()。若兩 Point 物件引用相同 (x, y) 座標,Point 雜湊值源自 x、y 座標值的 IEEE 754 位元表示,則它們相等。
對較複雜類,equals()、hashCode() 行為甚至可能受超類或介面影響。例如 List 介面要求:若且唯有另一物件是 List,且它們有相同順序相同元素(由元素上 Object.equals() 定義),List 物件等於另一物件。hashCode() 需求更特殊——list 的 hashCode() 值必須符合以下計算:
hashCode = 1;
Iterator i = list.iterator();
while (i.hasNext()) {
Object obj = i.next();
hashCode = 31*hashCode + (obj==null ? 0 : obj.hashCode());
}
不僅雜湊值取決於 list 內容,還規定結合各元素雜湊值的特殊演算法。(String 類規定類似演算法計算字串雜湊值。)
覆寫預設 equals() 方法較簡單,但若不違反對稱性或傳遞性需求,覆寫已覆寫的 equals() 極其棘手。覆寫 equals() 時,應總在 equals() 中包含一些 Javadoc 註釋,幫助希望正確擴展您類的用戶。
作為簡單範例,考慮以下類:
class A {
final B someNonNullField;
C someOtherField;
int someNonStateField;
}
我們應如何編寫該類 equals() 方法?此法適用許多情況:
public boolean equals(Object other) {
// Not strictly necessary, but often a good optimization
if (this == other) return true;
if (!(other instanceof A)) return false;
A otherA = (A) other;
return
(someNonNullField.equals(otherA.someNonNullField))
&& ((someOtherField == null)
? otherA.someOtherField == null
: someOtherField.equals(otherA.someOtherField)));
}
現在定義了 equals(),必須以一致方式定義 hashCode()。一種一致但非總有效的定義 hashCode() 方法:
public int hashCode() { return 0; }
此法將生成大量碰撞並顯著降低 HashMap 效能,但符合規範。一個更合理的 hashCode() 實作應如下:
public int hashCode() {
int hash = 1;
hash = hash * 31 + someNonNullField.hashCode();
hash = hash * 31
+ (someOtherField == null ? 0 : someOtherField.hashCode());
return hash;
}
注意:這兩實作都降低類狀態欄位的 equals() 或 hashCode() 方法一定比例計算能力。根據您使用的類,可能希望降低超類的 equals() 或 hashCode() 功能一部分計算能力。對基本欄位,相關包裝類有 helper 功能協助創建雜湊值,如 Float.floatToIntBits。
編寫完美 equals() 方法不現實。通常,擴展自身覆寫 equals() 的可實例化類時,覆寫 equals() 不切實際,且編寫將被覆寫的 equals() 方法(如在抽象類中)不同於為具體類編寫 equals()。更詳細資訊請參閱 Effective Java Programming Language Guide Item 7(見參考資料)。
將雜湊法構建到 Java 類庫根物件類是非常明智的設計折衷——使使用基於雜湊容器如此簡單高效。但人們對 Java 類庫雜湊演算法與物件相等性方法實作提出許多批評。java.util 基於雜湊容器非常方便易用,但可能不適用於需要極高效能應用。雖大部分不會改變,但設計嚴重依賴基於雜湊容器效率的應用時必須考慮:
int 而非 long 作為 hashCode() 回傳型別增加雜湊碰撞機率。String、List 定義將其元素雜湊值結合為一雜湊值的演算法,但語言規範未定義將多物件雜湊值結合為新雜湊值的任何認可方法。前面討論的 List、String 或實例類 A 使用的技巧都簡單,但算術上遠非完美。類庫未提供任何雜湊演算法方便實作,可簡化更先進 hashCode() 實作創建。equals() 的可實例化類時,定義 equals()「顯而易見」方式都不能滿足對稱或傳遞性需求。意味著覆寫 equals() 時,必須了解正在擴展類的結構與實作細節,甚至需暴露基類機密欄位,違反面向物件設計原則。透過統一定義 equals()、hashCode(),可提升類作為基於雜湊集合鍵值的使用性。定義物件相等性與雜湊值有兩種方法:基於身份(Object 提供預設)、基於狀態(要求覆寫 equals()、hashCode())。當物件狀態更改時若物件雜湊值發生變化,確信當狀態作為雜湊鍵值使用時不允許更改其狀態。