每個Java對象都有 hashCode() 和 equals() 方法。許多類忽略(Override)這些方法的預設實施,以在對象執行個體之間提供更深層次的語義可比性。在 Java理念和實踐這一部分,Java開發人員Brian Goetz向您介紹在建立Java類以有效和準確定義 hashCode() 和 equals() 時應遵循的規則和指南。您可以在
討論論壇與作者和其它讀者一同探討您對本文的看法。(您還可以點擊本文頂部或底部的 討論進入論壇。) 雖然Java語言不直接支援關聯陣列 -- 可以使用任何對象作為一個索引的數組 -- 但在根 Object 類中使用 hashCode() 方法明確表示期望廣泛使用 HashMap (及其前輩 Hashtable )。理想情況下基於散列的容器提供有效插入和有效檢索;直接在對象模式中支援散列可以促進基於散列的容器的開發和使用。
定義對象的相等性HashCode重載不當造成記憶體流失
Object 類有兩種方法來推斷對象的標識: equals() 和 hashCode() 。一般來說,如果您忽略了其中一種,您必須同時忽略這兩種,因為兩者之間有必須維持的至關重要的關係。特殊情況是根據 equals() 方法,如果兩個對象是相等的,它們必須有相同的 hashCode() 值(儘管這通常不是真的)。
特定類的 equals() 的語義在Implementer的左側定義;定義對特定類來說 equals() 意味著什麼是其設計工作的一部分。 Object 提供的預設實施簡單引用下面等式:
在這種預設實施情況下,只有它們引用真正同一個對象時這兩個引用才是相等的。同樣, Object 提供的 hashCode() 的預設實施通過將對象的記憶體位址對映於一個整數值來產生。由於在某些架構上,地址空間大於 int 值的範圍,兩個不同的對象有相同的 hashCode() 是可能的。如果您忽略了 hashCode() ,您仍舊可以使用 System.identityHashCode() 方法來接入這類預設值。
忽略equals() -- 簡單一實例
預設情況下, equals() 和 hashCode() 基於標識的實施是合理的,但對於某些類來說,它們希望放寬等式的定義。例如, Integer 類定義 equals() 與下面類似:
在這個定義中,只有在包含相同的整數值的情況下這兩個 Integer 對象是相等的。結合將不可修改的 Integer ,這使得使用 Integer 作為 HashMap 中的關鍵字是切實可行的。這種基於值的Equal方法可以由Java類庫中的所有原始封裝類使用,如 Integer 、 Float 、 Character 和 Boolean 以及 String (如果兩個 String 對象包含相同順序的字元,那它們是相等的)。由於這些類都是不可修改的並且可以實施 hashCode() 和 equals() ,它們都可以做為很好的散列關鍵字。
為什麼忽略equals()和hashCode()?
如果 Integer 不忽略 equals() 和 hashCode() 情況又將如何?如果我們從未在 HashMap 或其它基於散列的集合中使用 Integer 作為關鍵字的話,什麼也不會發生。但是,如果我們在 HashMap中 使用這類 Integer 對象作為關鍵字,我們將不能夠可靠地檢索相關的值,除非我們在 get() 調用中使用與 put() 調用中極其類似的 Integer 執行個體。這要求確保在我們的整個程式中,只能使用對應於特定整數值的 Integer 對象的一個執行個體。不用說,這種方法極不方便而且錯誤頻頻。
Object 的interface contract要求如果根據 equals() 兩個對象是相等的,那麼它們必須有相同的 hashCode() 值。當其識別能力整個包含在 equals() 中時,為什麼我們的根對象類需要 hashCode() 。 hashCode() 方法純粹用於提高效率。Java平台設計人員預計到了典型Java應用程式中基於散列的集合類(Collection Class)的重要性--如 Hashtable 、 HashMap 和 HashSet ,並且使用 equals() 與許多個物件進行比較在計算方面非常昂貴。使所有Java對象都能夠支援 hashCode() 並結合使用基於散列的集合,可以實現有效儲存和檢索。
實施equals()和hashCode()的需求
實施 equals() 和 hashCode() 有一些限制, Object 檔案中列舉出了這些限制。特別是 equals() 方法必須顯示以下屬性: Symmetry:兩個引用, a 和 b , a.equals(b) if and only if b.equals(a) Reflexivity:所有非Null 參考, a.equals(a) Transitivity:If a.equals(b) and b.equals(c) , then a.equals(c) Consistency with hashCode() :兩個相等的對象必須有相同的 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-bit表示,那麼它們是相等的。
對於比較複雜的類來說, equals() 和 hashCode() 的行為可能甚至受到superclass或interface的影響。例如, List 介面要求如果並且只有另一個對象是 List, 而且它們有相同順序的相同的Elements(由Element上的 Object.equals() 定義), List 對象等於另一個對象。 hashCode() 的需求更特殊--list的 hashCode() 值必須符合以下計算:
不僅僅散列值取決於list的內容,而且還規定了結合各個Element的散列值的特殊演算法。( String 類規定類似的演算法用於計算 String 的散列值。)
編寫自己的equals()和hashCode()方法
忽略預設的 equals() 方法比較簡單,但如果不違反對稱(Symmetry)或傳遞性(Transitivity)需求,忽略已經忽略的 equals() 方法極其棘手。當忽略 equals() 時,您應該總是在 equals() 中包括一些Javadoc注釋,以協助那些希望能夠正確擴充您的類的使用者。
作為一個簡單的例子,考慮以下類:
我們應如何編寫該類的 equals() 的方法。這種方法適用於許多情況:
現在我們定義了 equals() ,我們必須以統一的方法來定義 hashCode() 。一種統一但並不總是有效定義 hashCode() 的方法如下:
這種方法將產生大量的條目並顯著降低 HashMap s的效能,但它符合規範。一個更合理的 hashCode() 實施應該是這樣:
注意:這兩種實施都降低了類狀態欄位的 equals() 或 hashCode() 方法一定比例的計算能力。根據您使用的類,您可能希望降低superclass的 equals() 或 hashCode() 功能一部分計算能力。對於原始欄位來說,在相關的封裝類中有helper功能,可以協助建立散列值,如 Float.floatToIntBits 。
編寫一個完美的 equals() 方法是不現實的。通常,當擴充一個自身忽略了 equals() 的instantiable類時,忽略 equals() 是不切實際的,而且編寫將被忽略的 equals() 方法(如在抽象類別中)不同於為具體類編寫 equals() 方法。關於執行個體以及說明的更詳細資料請參閱 Effective Java Programming Language Guide, Item 7 ( 參考資料) 。
有待改進?
將散列法構建到Java類庫的根對象類中是一種非常明智的設計折衷方法 -- 它使使用基於散列的容器變得如此簡單和高效。但是,人們對Java類庫中的散列演算法和對象相等性的方法和實施提出了許多批評。 java.util 中基於散列的容器非常方便和簡便易用,但可能不適用於需要非常高效能的應用程式。雖然其中大部分將不會改變,但當您設計嚴重依賴於基於散列的容器效率的應用程式時必須考慮這些因素,它們包括: 太小的散列範圍。使用 int 而不是 long 作為 hashCode() 的傳回型別增加了散列衝突的幾率。
糟糕的散列值分配。短strings和小型integers的散列值是它們自己的小整數,接近於其它“鄰近”對象的散列值。一個循規導矩(Well-behaved)的散列函數將在該散列範圍內更均勻地分配散列值。 無定義的散列操作。雖然某些類,如 String 和 List ,定義了將其Element的散列值結合到一個散列值中使用的散列演算法,但語言規範不定義將多個對象的散列值結合到新散列值中的任何獲批准的方法。我們在前面 編寫自己的equals()和hashCode()方法中討論的 List 、 String 或執行個體類 A 使用的訣竅都很簡單,但算術上還遠遠不夠完美。類庫不提供任何散列演算法的方便實施,它可以簡化更先進的 hashCode() 實施的建立。
當擴充已經忽略了 equals() 的 instantiable類時很難編寫 equals() 。當擴充已經忽略了 equals() 的 instantiable類時,定義 equals() 的“顯而易見的”方式都不能滿足 equals() 方法的對稱或傳遞性需求。這意味著當忽略 equals() 時,您必須瞭解您正在擴充的類的結構和實施詳細資料,甚至需要暴露基本類中的機密欄位,它違反了物件導向的設計的原則。
結束語
通過統一定義 equals() 和 hashCode(), 您可以提升類作為基於散列的集合中的關鍵字的使用性。有兩種方法來定義對象的相等性和散列值:基於標識,它是 Object 提供的預設方法;基於狀態,它要求忽略 equals() 和 hashCode() 。當對象的狀態更改時如果對象的散列值發生變化,確信當狀態作為散列關鍵字使用時您不允許更更改其狀態
關於作者
|
| |
|
Brian Goetz過去15年以來一直是專業軟體開發人員。 |