java中equals()和hashcode()的關係

雖然Java語言不直接支持關聯數組 -- 可以使用任何對象作爲一個索引的數組 -- 但在根 Object 類中使用 hashCode() 方法明確表示期望廣泛使用 HashMap (及其前輩 Hashtable )。理想情況下基於散列的容器提供有效插入和有效檢索;直接在對象模式中支持散列可以促進基於散列的容器的開發和使用。

定義對象的相等性

Object 類有兩種方法來推斷對象的標識: equals()hashCode() 。一般來說,如果您忽略了其中一種,您必須同時忽略這兩種,因爲兩者之間有必須維持的至關重要的關係。特殊情況是根據 equals() 方法,如果兩個對象是相等的,它們必須有相同的 hashCode() 值(儘管這通常不是真的)。

特定類的 equals() 的語義在Implementer的左側定義;定義對特定類來說 equals() 意味着什麼是其設計工作的一部分。 Object 提供的缺省實施簡單引用下面等式:

  public boolean equals(Object obj) { return (this == obj); }

 

在這種缺省實施情況下,只有它們引用真正同一個對象時這兩個引用纔是相等的。同樣, Object 提供的 hashCode() 的缺省實施通過將對象的內存地址對映於一個整數值來生成。由於在某些架構上,地址空間大於 int 值的範圍,兩個不同的對象有相同的 hashCode() 是可能的。如果您忽略了 hashCode() ,您仍舊可以使用 System.identityHashCode() 方法來接入這類缺省值。

忽略equals() -- 簡單實例

缺省情況下, equals()hashCode() 基於標識的實施是合理的,但對於某些類來說,它們希望放寬等式的定義。例如, Integer 類定義 equals() 與下面類似:

  public boolean equals(Object obj) {
return (obj instanceof Integer
&& intValue() == ((Integer) obj).intValue());
}

 

在這個定義中,只有在包含相同的整數值的情況下這兩個 Integer 對象是相等的。結合將不可修改的 Integer ,這使得使用 Integer 作爲 HashMap 中的關鍵字是切實可行的。這種基於值的Equal方法可以由Java類庫中的所有原始封裝類使用,如 IntegerFloatCharacterBoolean 以及 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)的重要性--如 HashtableHashMapHashSet ,並且使用 equals() 與許多對象進行比較在計算方面非常昂貴。使所有Java對象都能夠支持 hashCode() 並結合使用基於散列的集合,可以實現有效的存儲和檢索。

 




回頁首

 

實施equals()和hashCode()的需求

實施 equals()hashCode() 有一些限制, Object 文件中列舉出了這些限制。特別是 equals() 方法必須顯示以下屬性:

  • Symmetry:兩個引用, ab , a.equals(b) if and only if b.equals(a)
  • Reflexivity:所有非空引用, 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)?沒有簡單的答案 -- 它取決於類的計劃使用。對於象 ListMap 這樣的容器來說,人們對此爭論不已。Java類庫中的大多數類,包括容器類,錯誤出現在根據對象狀態來提供 equals()hashCode() 實施。

如果對象的 hashCode() 值可以基於其狀態進行更改,那麼當使用這類對象作爲基於散列的集合中的關鍵字時我們必須注意,確保當它們用於作爲散列關鍵字時,我們並不允許更改它們的狀 態。所有基於散列的集合假設,當對象的散列值用於作爲集合中的關鍵字時它不會改變。如果當關鍵字在集合中時它的散列代碼被更改,那麼將產生一些不可預測和 容易混淆的結果。實踐過程中這通常不是問題 -- 我們並不經常使用象 List 這樣的可修改對象做爲 HashMap 中的關鍵字。

一個簡單的可修改類的例子是Point,它根據狀態來定義 equals()hashCode() 。如果兩個 Point 對象引用相同的 (x, y) 座標, Point 的散列值來源於 xy 座標值的IEEE 754-bit表示,那麼它們是相等的。

對於比較複雜的類來說, equals()hashCode() 的行爲可能甚至受到superclass或interface的影響。例如, List 接口要求如果並且只有另一個對象是 List, 而且它們有相同順序的相同的Elements(由Element上的 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的內容,而且還規定了結合各個Element的散列值的特殊算法。( String 類規定類似的算法用於計算 String 的散列值。)

 




回頁首

 

編寫自己的equals()和hashCode()方法

忽略缺省的 equals() 方法比較簡單,但如果不違反對稱(Symmetry)或傳遞性(Transitivity)需求,忽略已經忽略的 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 s的性能,但它符合規範。一個更合理的 hashCode() 實施應該是這樣:

  public int hashCode() { 
int hash = 1;
hash = hash * 31 + someNonNullField.hashCode();
hash = hash * 31
+ (someOtherField == null ? 0 : someOtherField.hashCode());
return hash;
}

 

注意:這兩種實施都降低了類狀態字段的 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)的散列函數將在該散列範圍內更均勻地分配散列值。
  • 無定義的散列操作。雖然某些類,如 StringList ,定義了將其Element的散列值結合到一個散列值中使用的散列算法,但語言規範不定義將多個對象的散列值結合到新散列值中的任何批准的方法。我們在前面 編寫自己的equals()和hashCode()方法中討論的 ListString 或實例類 A 使用的訣竅都很簡單,但算術上還遠遠不夠完美。類庫不提供任何散列算法的方便實施,它可以簡化更先進的 hashCode() 實施的創建。
  • 當擴展已經忽略了 equals() 的 instantiable類時很難編寫 equals() 。當擴展已經忽略了 equals() 的 instantiable類時,定義 equals() 的“顯而易見的”方式都不能滿足 equals() 方法的對稱或傳遞性需求。這意味着當忽略 equals() 時,您必須瞭解您正在擴展的類的結構和實施詳細信息,甚至需要暴露基本類中的機密字段,它違反了面向對象的設計的原則。
發佈了17 篇原創文章 · 獲贊 2 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章