雖然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()
和
hashCode()
基於標識的實施是合理的,但對於某些類來說,它們希望放寬等式的定義。例如,
Integer
類定義
equals()
與下面類似:
public boolean equals(Object obj) { |
在這個定義中,只有在包含相同的整數值的情況下這兩個
Integer
對象是相等的。結合將不可修改的
Integer
,這使得使用
Integer
作爲
HashMap
中的關鍵字是切實可行的。這種基於值的Equal方法可以由Java類庫中的所有原始封裝類使用,如
Integer
、
Float
、
Character
和
Boolean
以及
String
(如果兩個
String
對象包含相同順序的字符,那它們是相等的)。由於這些類都是不可修改的並且可以實施
hashCode()
和
equals()
,它們都可以做爲很好的散列關鍵字。
如果
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()
有一些限制,
Object
文件中列舉出了這些限制。特別是
equals()
方法必須顯示以下屬性:
- Symmetry:兩個引用,
a
和b
,a.equals(b) if and only if b.equals(a)
- Reflexivity:所有非空引用,
a.equals(a)
- Transitivity:If
a.equals(b)
andb.equals(c)
, thena.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; |
不僅僅散列值取決於list的內容,而且還規定了結合各個Element的散列值的特殊算法。(
String
類規定類似的算法用於計算
String
的散列值。)
|
忽略缺省的
equals()
方法比較簡單,但如果不違反對稱(Symmetry)或傳遞性(Transitivity)需求,忽略已經忽略的
equals()
方法極其棘手。當忽略
equals()
時,您應該總是在
equals()
中包括一些Javadoc註釋,以幫助那些希望能夠正確擴展您的類的用戶。
作爲一個簡單的例子,考慮以下類:
class A { |
我們應如何編寫該類的
equals()
的方法?這種方法適用於許多情況:
public boolean equals(Object other) { |
現在我們定義了
equals()
,我們必須以統一的方法來定義
hashCode()
。一種統一但並不總是有效的定義
hashCode()
的方法如下:
public int hashCode() { return 0; } |
這種方法將生成大量的條目並顯著降低
HashMap
s的性能,但它符合規範。一個更合理的
hashCode()
實施應該是這樣:
public int 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()
時,您必須瞭解您正在擴展的類的結構和實施詳細信息,甚至需要暴露基本類中的機密字段,它違反了面向對象的設計的原則。