雖然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()
和 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
若是 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()
有一些限制, Object
文件中列舉出了這些限制。特別是 equals()
方法必須顯示如下屬性:
a
和 b
, a.equals(b) if and only if b.equals(a)
a.equals(a)
a.equals(b)
and b.equals(c)
, then a.equals(c)
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()
方法比較簡單,但若是不違反對稱(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()
的返回類型增長了散列衝突的概率。 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