有效和正肯定義hashCode()和equals()

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

 

定義對象的相等性算法

 

Object 類有兩種方法來推斷對象的標識: equals()  hashCode() 。通常來講,若是您忽略了其中一種,您必須同時忽略這兩種,由於二者之間有必須維持的相當重要的關係。特殊狀況是根據 equals() 方法,若是兩個對象是相等的,它們必須有相同的 hashCode() 值(儘管這一般不是真的)。數組

 

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

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


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

 

忽略equals() -- 簡單實例函數

 

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

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


在這個定義中,只有在包含相同的整數值的狀況下這兩個 Integer 對象是相等的。結合將不可修改的 Integer ,這使得使用 Integer 做爲 HashMap 中的關鍵字是切實可行的。這種基於值的Equal方法能夠由Java類庫中的全部原始封裝類使用,如 Integer  Float  Character  Boolean 以及 String (若是兩個 String 對象包含相同順序的字符,那它們是相等的)。因爲這些類都是不可修改的而且能夠實施 hashCode()  equals() ,它們均可以作爲很好的散列關鍵字。flex


爲何忽略equals()和hashCode()?this

 

若是 Integer 不忽略 equals()  hashCode() 狀況又將如何?若是咱們從未在 HashMap 或其它基於散列的集合中使用 Integer 做爲關鍵字的話,什麼也不會發生。可是,若是咱們在 HashMap中 使用這類 Integer 對象做爲關鍵字,咱們將不可以可靠地檢索相關的值,除非咱們在 get() 調用中使用與 put() 調用中極其相似的 Integer 實例。這要求確保在咱們的整個程序中,只能使用對應於特定整數值的 Integer 對象的一個實例。不用說,這種方法極不方便並且錯誤頻頻。spa

 

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:全部非空引用, 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() 值必須符合如下計算:

 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() 方法。






有待改進?

 

將散列法構建到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()。當對象的狀態更改時若是對象的散列值發生變化,確信當狀態做爲散列關鍵字使用時您不容許更更改其狀態。

 

 

 

轉自http://www.ibm.com/developerworks/cn/java/j-jtp05273

相關文章
相關標籤/搜索