每個(gè)Java對象都有hashCode()和 equals()方法。許多類(lèi)忽略(Override)這些方法的缺省實(shí)施,以在對象實(shí)例之間提供更深層次的語(yǔ)義可比性。在Java理念和實(shí)踐這一部分,Java開(kāi)發(fā)人員Brian Goetz向您介紹在創(chuàng )建Java類(lèi)以有效和準確定義hashCode()和equals()時(shí)應遵循的規則和指南。您可以在討論論壇與作者和其它讀者一同探討您對本文的看法。(您還可以點(diǎn)擊本文頂部或底部的討論進(jìn)入論壇。)
雖然Java語(yǔ)言不直接支持關(guān)聯(lián)數組 -- 可以使用任何對象作為一個(gè)索引的數組 -- 但在根Object類(lèi)中使用hashCode()方法明確表示期望廣泛使用HashMap(及其前輩Hashtable)。理想情況下基于散列的容器提供有效插入和有效檢索;直接在對象模式中支持散列可以促進(jìn)基于散列的容器的開(kāi)發(fā)和使用。
定義對象的相等性
Object類(lèi)有兩種方法來(lái)推斷對象的標識:equals()和hashCode()。一般來(lái)說(shuō),如果您忽略了其中一種,您必須同時(shí)忽略這兩種,因為兩者之間有必須維持的至關(guān)重要的關(guān)系。特殊情況是根據equals() 方法,如果兩個(gè)對象是相等的,它們必須有相同的hashCode()值(盡管這通常不是真的)。
特定類(lèi)的equals()的語(yǔ)義在Implementer的左側定義;定義對特定類(lèi)來(lái)說(shuō)equals()意味著(zhù)什么是其設計工作的一部分。Object提供的缺省實(shí)施簡(jiǎn)單引用下面等式:
public boolean equals(Object obj) { return (this == obj); }
在這種缺省實(shí)施情況下,只有它們引用真正同一個(gè)對象時(shí)這兩個(gè)引用才是相等的。同樣,Object提供的hashCode()的缺省實(shí)施通過(guò)將對象的內存地址對映于一個(gè)整數值來(lái)生成。由于在某些架構上,地址空間大于int值的范圍,兩個(gè)不同的對象有相同的hashCode()是可能的。如果您忽略了hashCode(),您仍舊可以使用System.identityHashCode()方法來(lái)接入這類(lèi)缺省值。
忽略 equals() -- 簡(jiǎn)單實(shí)例
缺省情況下,equals()和hashCode()基于標識的實(shí)施是合理的,但對于某些類(lèi)來(lái)說(shuō),它們希望放寬等式的定義。例如,Integer類(lèi)定義equals() 與下面類(lèi)似:
public boolean equals(Object obj) {
return (obj instanceof Integer
&& intValue() == ((Integer) obj).intValue());
}
在這個(gè)定義中,只有在包含相同的整數值的情況下這兩個(gè)Integer對象是相等的。結合將不可修改的Integer,這使得使用Integer作為HashMap中的關(guān)鍵字是切實(shí)可行的。這種基于值的Equal方法可以由Java類(lèi)庫中的所有原始封裝類(lèi)使用,如Integer、Float、Character和Boolean以及String(如果兩個(gè)String對象包含相同順序的字符,那它們是相等的)。由于這些類(lèi)都是不可修改的并且可以實(shí)施hashCode()和equals(),它們都可以做為很好的散列關(guān)鍵字。
為什么忽略 equals()和hashCode()?
如果Integer不忽略equals() 和 hashCode()情況又將如何?如果我們從未在HashMap或其它基于散列的集合中使用Integer作為關(guān)鍵字的話(huà),什么也不會(huì )發(fā)生。但是,如果我們在HashMap中使用這類(lèi)Integer對象作為關(guān)鍵字,我們將不能夠可靠地檢索相關(guān)的值,除非我們在get()調用中使用與put()調用中極其類(lèi)似的Integer實(shí)例。這要求確保在我們的整個(gè)程序中,只能使用對應于特定整數值的Integer對象的一個(gè)實(shí)例。不用說(shuō),這種方法極不方便而且錯誤頻頻。
Object的interface contract要求如果根據 equals()兩個(gè)對象是相等的,那么它們必須有相同的hashCode()值。當其識別能力整個(gè)包含在equals()中時(shí),為什么我們的根對象類(lèi)需要hashCode()?hashCode()方法純粹用于提高效率。Java平臺設計人員預計到了典型Java應用程序中基于散列的集合類(lèi)(Collection Class)的重要性--如Hashtable、HashMap和HashSet,并且使用equals()與許多對象進(jìn)行比較在計算方面非常昂貴。使所有Java對象都能夠支持 hashCode()并結合使用基于散列的集合,可以實(shí)現有效的存儲和檢索。
實(shí)施equals()和hashCode()的需求
實(shí)施equals()和 hashCode()有一些限制,Object文件中列舉出了這些限制。特別是equals()方法必須顯示以下屬性:
Symmetry:兩個(gè)引用,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():兩個(gè)相等的對象必須有相同的hashCode()值
Object的規范中并沒(méi)有明確要求equals()和 hashCode() 必須一致 -- 它們的結果在隨后的調用中將是相同的,假設“不改變對象相等性比較中使用的任何信息。”這聽(tīng)起來(lái)象“計算的結果將不改變,除非實(shí)際情況如此。”這一模糊聲明通常解釋為相等性和散列值計算應是對象的可確定性功能,而不是其它。
對象相等性意味著(zhù)什么?
人們很容易滿(mǎn)足Object類(lèi)規范對equals() 和 hashCode() 的要求。決定是否和如何忽略equals()除了判斷以外,還要求其它。在簡(jiǎn)單的不可修值類(lèi)中,如Integer(事實(shí)上是幾乎所有不可修改的類(lèi)),選擇相當明顯 -- 相等性應基于基本對象狀態(tài)的相等性。在Integer情況下,對象的唯一狀態(tài)是基本的整數值。
對于可修改對象來(lái)說(shuō),答案并不總是如此清楚。equals() 和hashCode() 是否應基于對象的標識(象缺省實(shí)施)或對象的狀態(tài)(象Integer和String)?沒(méi)有簡(jiǎn)單的答案 -- 它取決于類(lèi)的計劃使用。對于象List和Map這樣的容器來(lái)說(shuō),人們對此爭論不已。Java類(lèi)庫中的大多數類(lèi),包括容器類(lèi),錯誤出現在根據對象狀態(tài)來(lái)提供equals()和hashCode()實(shí)施。
如果對象的hashCode()值可以基于其狀態(tài)進(jìn)行更改,那么當使用這類(lèi)對象作為基于散列的集合中的關(guān)鍵字時(shí)我們必須注意,確保當它們用于作為散列關(guān)鍵字時(shí),我們并不允許更改它們的狀態(tài)。所有基于散列的集合假設,當對象的散列值用于作為集合中的關(guān)鍵字時(shí)它不會(huì )改變。如果當關(guān)鍵字在集合中時(shí)它的散列代碼被更改,那么將產(chǎn)生一些不可預測和容易混淆的結果。實(shí)踐過(guò)程中這通常不是問(wèn)題 -- 我們并不經(jīng)常使用象List這樣的可修改對象做為HashMap中的關(guān)鍵字。
一個(gè)簡(jiǎn)單的可修改類(lèi)的例子是Point,它根據狀態(tài)來(lái)定義equals()和hashCode()。如果兩個(gè)Point 對象引用相同的(x, y)座標,Point的散列值來(lái)源于x和y座標值的IEEE 754-bit表示,那么它們是相等的。
對于比較復雜的類(lèi)來(lái)說(shuō),equals()和hashCode()的行為可能甚至受到superclass或interface的影響。例如,List接口要求如果并且只有另一個(gè)對象是List,而且它們有相同順序的相同的Elements(由Element上的Object.equals() 定義),List對象等于另一個(gè)對象。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的內容,而且還規定了結合各個(gè)Element的散列值的特殊算法。(String類(lèi)規定類(lèi)似的算法用于計算String的散列值。)
編寫(xiě)自己的equals()和hashCode()方法
忽略缺省的equals()方法比較簡(jiǎn)單,但如果不違反對稱(chēng)(Symmetry)或傳遞性(Transitivity)需求,忽略已經(jīng)忽略的equals() 方法極其棘手。當忽略equals()時(shí),您應該總是在equals()中包括一些Javadoc注釋?zhuān)詭椭切┫M軌蛘_擴展您的類(lèi)的用戶(hù)。
作為一個(gè)簡(jiǎn)單的例子,考慮以下類(lèi):
class A {
final B someNonNullField;
C someOtherField;
int someNonStateField;
}
我們應如何編寫(xiě)該類(lèi)的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(),我們必須以統一的方法來(lái)定義hashCode()。一種統一但并不總是有效的定義hashCode()的方法如下:
public int hashCode() { return 0; }
這種方法將生成大量的條目并顯著(zhù)降低HashMaps的性能,但它符合規范。一個(gè)更合理的hashCode()實(shí)施應該是這樣:
public int hashCode() {
int hash = 1;
hash = hash * 31 + someNonNullField.hashCode();
hash = hash * 31
+ (someOtherField == null ? 0 : someOtherField.hashCode());
return hash;
}
注意:這兩種實(shí)施都降低了類(lèi)狀態(tài)字段的equals()或hashCode()方法一定比例的計算能力。根據您使用的類(lèi),您可能希望降低superclass的equals()或hashCode()功能一部分計算能力。對于原始字段來(lái)說(shuō),在相關(guān)的封裝類(lèi)中有helper功能,可以幫助創(chuàng )建散列值,如Float.floatToIntBits。
編寫(xiě)一個(gè)完美的equals()方法是不現實(shí)的。通常,當擴展一個(gè)自身忽略了equals()的instantiable類(lèi)時(shí),忽略equals()是不切實(shí)際的,而且編寫(xiě)將被忽略的equals()方法(如在抽象類(lèi)中)不同于為具體類(lèi)編寫(xiě)equals()方法。關(guān)于實(shí)例以及說(shuō)明的更詳細信息請參閱Effective Java Programming Language Guide, Item 7 (參考資料) 。
有待改進(jìn)?
將散列法構建到Java類(lèi)庫的根對象類(lèi)中是一種非常明智的設計折衷方法 -- 它使使用基于散列的容器變得如此簡(jiǎn)單和高效。但是,人們對Java類(lèi)庫中的散列算法和對象相等性的方法和實(shí)施提出了許多批評。java.util中基于散列的容器非常方便和簡(jiǎn)便易用,但可能不適用于需要非常高性能的應用程序。雖然其中大部分將不會(huì )改變,但當您設計嚴重依賴(lài)于基于散列的容器效率的應用程序時(shí)必須考慮這些因素,它們包括:
太小的散列范圍。使用int而不是long作為hashCode()的返回類(lèi)型增加了散列沖突的幾率。
糟糕的散列值分配。短strings和小型integers的散列值是它們自己的小整數,接近于其它“鄰近”對象的散列值。一個(gè)循規導矩(Well-behaved)的散列函數將在該散列范圍內更均勻地分配散列值。
無(wú)定義的散列操作。雖然某些類(lèi),如String和List,定義了將其Element的散列值結合到一個(gè)散列值中使用的散列算法,但語(yǔ)言規范不定義將多個(gè)對象的散列值結合到新散列值中的任何批準的方法。我們在前面編寫(xiě)自己的equals()和hashCode()方法中討論的List、String或實(shí)例類(lèi)A使用的訣竅都很簡(jiǎn)單,但算術(shù)上還遠遠不夠完美。類(lèi)庫不提供任何散列算法的方便實(shí)施,它可以簡(jiǎn)化更先進(jìn)的hashCode()實(shí)施的創(chuàng )建。
當擴展已經(jīng)忽略了equals()的 instantiable類(lèi)時(shí)很難編寫(xiě)equals()。當擴展已經(jīng)忽略了equals()的 instantiable類(lèi)時(shí),定義equals()的“顯而易見(jiàn)的”方式都不能滿(mǎn)足equals()方法的對稱(chēng)或傳遞性需求。這意味著(zhù)當忽略equals()時(shí),您必須了解您正在擴展的類(lèi)的結構和實(shí)施詳細信息,甚至需要暴露基本類(lèi)中的機密字段,它違反了面向對象的設計的原則。
結束語(yǔ)
通過(guò)統一定義equals()和hashCode(),您可以提升類(lèi)作為基于散列的集合中的關(guān)鍵字的使用性。有兩種方法來(lái)定義對象的相等性和散列值:基于標識,它是Object提供的缺省方法;基于狀態(tài),它要求忽略equals()和hashCode()。當對象的狀態(tài)更改時(shí)如果對象的散列值發(fā)生變化,確信當狀態(tài)作為散列關(guān)鍵字使用時(shí)您不允許更更改其狀態(tài)。