轉自http://blog.sina.com.cn/s/blog_4a5ca024010008tj.html 下面是本文
小崔的翻譯處女座《Equals 方法和 HashCode方法》 (2007-06-22 21:04:42
小崔(民間亦稱崔兒;即CY,見聊天錄系列),湖南人,20多歲,未婚。曾任AJ*Q*051班小組長,現有翻譯大作 《Equals 方法和 HashCode方法》,在本部落格首發,大家歡迎。
Equals 方法和 HashCode方法
作者:Manish Hatwalne
原文地址:http://www.geocities.com/technofundo/tech/java/equalhash.html
前言
在Java語言中,所有類的父類 java.lang.Object 中有兩個非常重要的方法。
· public boolean equals(Object obj)
· public int hashCode()
在使用者類和其他類進行比較的時候或被添加到集合中的時候,這兩個方法的重要性就被體現出來了。這兩個方法已經成為了SCJP 1.4認證的題目。本文將向有志於SCJP 1.4認證的人提供一些關於這兩個方法的必要資訊,即使你對此認證並沒有興趣,也可以協助你理解這兩個方法的機制,並在你自己的類中實現這些方法。
public boolean equals(Object obj)
這個方法是用來判斷調用equals方法的對象與被當作參數傳入的其他對象是否相等。equals方法在Object類中的預設實現是僅僅是判斷兩個對象的引用x和y是否指向同一個對象即判斷x == y是否成立。這個特殊的比較即“淺比較”。然而,自己實現了equals方法的類應該實現“深比較”,即比較相應的資料內容。因為Object類中沒有屬性,所以使用“淺比較”做簡單實現。
以下是JDK 1.4 API文檔中關於Object類equals方法的協定:
指出某個其他對象是否與此對象“相等”。
equals方法(在非Null 物件引用上)實現相等關係:
· 自反性:對於任何(非空)引用值 x,x.equals(x) 都應返回 true。
· 對稱性:對於任何(非空)引用值 x 和 y,若且唯若 y.equals(x) 返回 true 時,x.equals(y) 才應返回 true。
· 傳遞性:對於任何(非空)引用值 x、y 和 z,如果 x.equals(y) 返回 true,並且 y.equals(z) 返回 true,那麼 x.equals(z) 應返回 true。
· 一致性:對於任何(非空)引用值 x 和 y,多次調用 x.equals(y) 始終返回 true 或始終返回 false,前提是對象上 equals 比較中所用的資訊沒有被修改。
· 對於任何非Null 參考值 x,x.equals(null) 都應返回 false。
Object 類的 equals 方法實現對象上差別可能性最大的相等關係;即,對於任何非Null 參考值 x 和 y,若且唯若 x 和 y 引用同一個對象時,此方法才返回 true(x == y 具有值 true)。
注意:當此方法被重寫時,通常有必要重寫 hashCode 方法,以維護 hashCode 方法的常規協定,該協定聲明相等對象必須具有相等的雜湊碼。
equals方法的約定正好描述了它的需求,一旦你完全理解了,正確的實現這個方法將變的非常容易。現在讓我們來分析一下每條的真實含義。
1. 自反性 - 它僅僅描述了在任何情況下,一個對象必須等於它本身。除非你故意以不同的方式實現equals方法。
2. 對稱性 - 如果一個類的對象與另一個類的對象相等,那麼另一個類的對象也必須等於這個類的對象。換句話說,一個對象不能單方面決定它和另一個對象相等。因此,屬於兩個類的兩個對象,必須從兩方面判斷他們是否相等。這兩者必須一致。因此,使用你自己類中的equals方法與java.lang.String類或者其他Java內建類的對象進行比較是不恰當且不正確的。完全理解這點是非常重要的,因為對equals方法的一個不恰當實現很有可能違背這條規則並導致一個非期望的結果。
3. 傳遞性 - 第一個對象等於第二個對象並且第二個對象等於第三個對象,那麼第一個對象等於第三個對象。換而言之,如果兩個對象都相等,根據對稱原則,它們中的一個對象不能決定它等於不同類型的另一個對象。所有的這三者不同的排列組合必須同時遵循對稱原則。看一下這個例子,A,B,C是三個類。A和B都以這樣的一種方式實現了equals方法:比較A類和B類對象的方法。現在,如果B類的作者決定修改他的equals方法,這個新實現的方法同樣可以比較C類,那麼他將違反傳遞原則。因為,在A類和C類的對象裡沒有存在合適的比較方法。
4. 一致性 – 如果兩個對象相等,只要它們沒有被修改,那麼它們必須保持這種相等關係。同樣地,如果他們不相等,只要它們沒有被修改,那麼它們也將保持這種不等關係。這種修改可能發生在它們中的任一個或兩者都被修改。
5. 與null比較 – 任何一個類的對象不等於null,因此當null被作為equals方法的參數傳入時,equals方法必須返回false。你得確保當null被作為equals方法的參數傳入時,你所實現的equals方法應該返回false。
6. Equals和雜湊碼的關係 – API文檔最後的注意事項非常重要,它描述了這兩個方法之間的關係。它說明了如果兩個對象相等,那麼他們必須有同樣的雜湊碼。然而,逆命題不成立。關於這點在文章的後面會有詳細討論。
public int hashCode()
這個方法將返回調用這個方法的對象的雜湊碼值。這個方法將返回整數形式的雜湊碼值,基於散列法的集合將使用這個傳回值,例如Hashtable, HashMap, HashSet等。如果equals方法被重寫,這個方法也必須被重寫。
以下是JDK 1.4 API文檔中關於Object類hashCode方法的聲明:
返回該對象的雜湊碼值。支援該方法是為雜湊表提供一些優點,例如,java.util.Hashtable 提供的雜湊表。
hashCode 的常規協定是:
· 在 Java 應用程式執行期間,在同一對象上多次調用 hashCode 方法時,必須一致地返回相同的整數,前提是對象上 equals 比較中所用的資訊沒有被修改。從某一應用程式的一次執行到同一應用程式的另一次執行,該整數無需保持一致。
· 如果根據 equals(Object) 方法,兩個對象是相等的,那麼在兩個對象中的每個對象上調用 hashCode 方法都必鬚生成相同的整數結果。
· 以下情況不是必需的:如果根據 equals(java.lang.Object) 方法,兩個對象不相等,那麼在兩個對象中的任一對象上調用 hashCode 方法必定會產生不同的整數結果。但是,程式員應該知道,為不相等的對象產生不同整數結果可以提高雜湊表的效能。
實際上,由 Object 類定義的 hashCode 方法確實會針對不同的對象返回不同的整數。(這一般是通過將該對象的內部地址轉換成一個整數來實現的,但是 JavaTM 程式設計語言不需要這種實現技巧。)
同equals方法的協定相比,hashCode方法的協定相對簡單而且容易理解。它聲明了在實現hashCode方法時的兩個重要條件。第三點協定其實是第二點的詳細描述。接下來理解一下這個協定的含義。
1. 同一次執行中的一致性 - 首先,協定聲明了hashCode方法返回的雜湊碼在應用程式的同一次執行中的多次調用必須是始終一樣的,只要對象中影響equals方法的資訊沒有被修改。
2. 雜湊碼和Equals的關係 – 協定中的第二點是說hashCode方法是equals方法的必要條件。它僅僅強調了同樣的關係——相等的對象必須含有相同的雜湊碼。第三點中詳細描述了不相等的對象並不一定需要有不同的雜湊碼。
在回顧了這兩個方法的常用約定之後,可以使用下面的語句清楚地概括兩者之間的關係。
相等的對象必須有相同的雜湊碼,不相等的對象並不一定需要有不同的雜湊碼。
這兩個方法約定中剩餘的條約沒有直接涉及這兩個方法之間的關係。那些條約之前已經討論過了。這種關係也強調了無論何時你重寫equals方法必須重寫hashCode方法。如果沒有按照這個約定,在使用Java集合類或者其他類的時候常常會導致不確定、不期望的結果。
正確的實現樣本
以下的代碼展示了如何完全實現equals方法和hashCode方法的約定,使其能夠協調地與其他Java類正常工作。這個類的equals方法使用和String等Java內建類型或者封裝類相似的方式進行實現:提供了和同類型對象的比較。 public class Test{ private int num; private String data; public boolean equals(Object obj){ if(this == obj) return true; if((obj == null) || (obj.getClass() != this.getClass())) return false; // 此處參數對象為Test類型 Test test = (Test)obj; return num == test.num && (data == test.data || (data != null && data.equals(test.data))); } public int hashCode(){ int hash = 7; hash = 31 * hash + num; hash = 31 * hash + (null == data ? 0 : data.hashCode()); return hash; } // 其他方法 }
現在,解釋一下為什麼說這個實現是正確的實現。Test類含有兩個成員變數: num和data. 這兩個變數定義了對象的屬性並且參與了這個類的對象的比較。因此,對象雜湊碼的計算中也應該涉及到這兩個變數。
首先來看equals方法。在第8行,我們可以看到被(當作參數)傳入的對象和它自己進行比較。如果兩個對象的引用指向堆區的同一個對象,在比較操作的代價非常昂貴的條件下這個判斷會節省很多時間。接下來,第10行的if條件會判斷參數是否為null,如果非null,然後(或操作符||是短路判斷)通過比較參數對象的類型和當前對象的類型來檢查參數是否是Type類型的。通過調用getClass()來獲得當前引用的類型。如果不滿足任一條件,方法將返回false。代碼如下:
if((obj == null) || (obj.getClass() != this.getClass())) return false; // 推薦
這個條件判斷應該是首選,而不是下面的這個:
if(!(obj instanceof Test)) return false; // 避免
這是因為,當參數是Test類的子類,第一種條件運算式判斷(藍色代碼的)確保會返回false。然而第二種條件運算式判斷(紅色代碼的)將出錯。如果參數是Test類的子類,含有instanceof操作符的判斷將不會返回false。因此它可能會違反約定中的對稱原則。(譯註:instanceof這種比較有可能出現多種情況,一種是不能轉型成子類而拋出異常,另一種是父類的private 成員在子類中不能使用因而不能進行比較。)但是如果類是final類型的,則instanceof 的檢查是正確的,因為那樣這個類將不會有子類。第一種條件運算式檢查可以在final類型或者非final類型的類裡正常工作。注意,如果參數為null,這兩種條件都回返回false。如果或操作符左邊的運算式是null,instanceof操作符會返回false,而不去考慮或操作符右邊的運算式。這在Java語言規範JLS 15.20.2中被指定。無論如何,第一種條件運算式檢查將是類型檢查的首選。
這個類實現的equals方法僅提供了相同類型對象的比較。注意,這不是強制的。但是,如果一個類提供了和其他類型對象比較的方法,那麼其他類型的類也同樣應該提供和這個類比較的方法,這是為了履行約定中的對稱性和自反性原則。equals方法的具體實現不應該違反雙方的要求。第14行和第15行實際展示了成員變數data的比較,如果值相同,將返回true。第15行也確保了在執行String類型的equals方法時不會出現null 指標異常。
在實現equals方法時,在經過一些必要的轉換之後,可以簡單的使用 == 操作符進行比較。(例如float和Float.floatToIntBits或者double和Double.doubleToLongBits的比較。) 然而,對象引用之間的比較會引起它們equals方法進行遞迴。你還需要確保在調用他們的equals方法時不會引發null 指標異常。
以下列舉了幾點正確實現equals方法的指導方針。
1. 如果參數為參考型別,使用 == 操作符進行比較。如果相等返回true. 在比較的代價很昂貴時這樣做將節省時間。
2. 使用下列的條件運算式判斷參數是否為null,類型是否合適。條件不滿足將返回false。
if((obj == null) || (obj.getClass() != this.getClass())) return false;
注意, 合適的類型並不意味著類型相同或者是程式碼範例中的類型。它可以是提供了比較方法的任一類型或介面。
3. 將參數類型轉換為合適的類型。再提一句,合適的類型可能並不是類型相同的。同樣,因為上面的類型檢查運算式,這步不會發生類型轉換異常。
4. 比較參數對象和當前對象中重要的變數是否相等,如果全部條件都滿足將返回true,否則返回false。正如之前提到的,當比較類的成員或變數時,基本類型的變數可以直接使用 == 操作符與經過必要轉換的變數進行比較(例如float和 Float.floatToIntBits或double to Double.doubleToLongBits)。然而,對象引用之間的比較會引起它們equals方法進行遞迴。你還需要確保在調用他們的equals方法時不會引發null 指標異常。正如執行個體代碼中15行所做的那樣。在比較中包含那些從其他變數計算出來的成員變數既不是必須的也不是明智的做法。這保證了equals方法的效能。僅僅由你決定類的成員中哪些是重要的哪些不是。
5. 不要改變equals方法的參數類型,它必須使用java.lang.Object作為參數類型,而不要使用你自訂的類型。如果你改變了參數類型,將不是重寫equals方法而是重載了這個方法。這個常見的錯誤會導致很多問題。因為這不是一個編譯錯誤,在你的代碼不能正常啟動並執行時候會使你很難找到問題的解決辦法。
6. 回顧你的equals方法檢查它是否全部實現了equals方法的全部約定。
7. 最後,當你重寫了equals方法不要忘記重寫hashCode方法,這可是不可原諒的錯誤。;)
現在,來看看樣本中的hashCode方法。在代碼的20行,一個非零的常數7(任意的)被賦值給變數hash。因為類的成員變數num和data參與了equals方法中的比較,它們也應該在雜湊碼的計算中被引入。儘管這並不是強制性的。你也可以使用參與計算equals方法的變數的子集來提高hashCode方法的效能。hashCode方法的效能確實非常重要,但是你也得小心挑選這個子集。這個子集應該包含那些最有可能產生最大差異值的變數。有時候,使用所有參與equals方法的變數將使雜湊碼的計算結果更有意義。範例程式碼中的類使用了num和data這兩個變數來計算雜湊碼。代碼中的21行和22行就是基於這兩個變數來計算雜湊碼的。第22行也確保了在執行hashCode方法時,如果data 變數為null不會導致null 指標異常。這個實現確保了沒有違反hashCode方法的約定。這個實現在多次調用hashCode方法時會返回一致的雜湊碼值,並且保證相等的對象返回相同的雜湊碼。在實現hashCode方法時,經過必要的轉換之後可以直接使用基本類型計算雜湊碼值。例如float和Float.floatToIntBits或者double和Double.doubleToLongBits。因為hashCode方法必須返回int型,所以long類型的值必須轉換為整型。至於參考型別的雜湊碼,調用這些對象的hashCode方法可以被計算出來。你應該確保在調用參考型別的hashCode方法不會導致null 指標異常。寫出一個優秀的hashCode方法實現不是一件容易的事情,它計算出來的雜湊碼應該是均衡分布的,這可能需要數學家和電腦理論科學家的參與。然而,遵循下列簡單的規則也可以寫出一個相當好的實現。
以下列舉了幾點正確實現hashCode方法的指導方針。
1. 為整型變數hash設定任意一個非零的整數(比方說 7)。
2. 在計算對象的雜湊碼的時候,需要包含參與計算equals方法的變數。為每個變數var計算一個單獨的雜湊碼var_code,應該遵循以下規則 -
a. 如果變數var是 byte,char,short或int類型,那麼var_code = (int)var;
b. 如果變數var是long類型,那麼var_code = (int)(var ^ (var >>> 32));
c. 如果變數var是float類型,那麼var_code = Float.floatToIntBits(var);
d. 如果變數var是double類型,那麼
long bits = Double.doubleToLongBits(var);
var_code = (int)(bits ^ (bits >>> 32));
e. 如果變數var是boolean,那麼 var_code = var ? 1 : 0;
f. 如果變數var是參考型別,首先檢查它是否為null,如果為null,var_code = 0; 否則執行參考型別的hashCode方法擷取雜湊碼。簡單一實例代碼如下:
var_code = (null == var ? 0 : var.hashCode());
3. 聯合這些單獨變數的雜湊碼var_code與原始雜湊碼變數hash做如下運算:
hash = 31 * hash + var_code;
4. 按照以上這些步驟計算對象中重要變數的雜湊碼值最後返回整型的計算結果hash
5. 最後,檢查你的hashCode方法並確認相等對象是否會返回相同的雜湊碼值。並且驗證同一次運行中在同一對象上多次調用多次調用hashCode方法返回的雜湊碼值是否相同。
這裡所提出的方針僅作為指導,並不是絕對的準則。不過遵循這些方針來實現這兩個方法的確會產生正確、一致的結果。
摘要與技巧
· 只要兩個對象相等,那麼這兩個相等的對象應該含有相同的雜湊碼。然而,不相等的對象不一定要有不同的雜湊碼。
· 和 == 操作符的“淺比較”不同的是equals方法通過檢查兩個對象是否在邏輯上相等的方法來實現“深比較”。
· 在java.lang.Object 類中equals方法仍然只提供了和 == 操作符相同的“淺比較”。
· equals方法的使用了Java物件類型作為參數而不是基本類型。調用equals方法時使用一個基本類型將會導致一個編譯錯誤。
· 在調用equals方法時傳入不同的參考型別不會導致編譯錯誤或執行階段錯誤。
· 對於java.lang.String類或標準的Java封裝類,如果使用equals方法時,參數類型和調用equals方法對象的類型不同,會返回false。
· java.lang.StringBuffer 類沒有覆蓋java.lang.Object 類的equals方法,它繼承了java.lang.Object 類的實現。
· 你實現的equals方法不能和任何Java內建類型進行比較,因為那樣會違反equals方法約定中的對稱性原則。
· 如果equals方法傳入的參數為null,傳回值為false。
· 具有相同雜湊碼值的對象並不意味著這兩個對象相等。
· return 1; 這是一個合法的hashCode方法實現,然而這是一種非常不推薦的實現。之所以說它合法,僅僅是因為它確保相等的對象含有相同的雜湊碼,確保在同一次運行中的多次調用會返回一致的雜湊碼。因而它沒有違反hashCode方法的常規約定。之所以說它是不好的實現,因為它為所有對象返回相同的雜湊碼。這個說明適用與所有返回相同整型值的hashCode方法實現。
· 在標準的JDK 1.4版本中,如下的幾個封裝類java.lang.Short, java.lang.Byte, java.lang.Character 和java.lang.Integer 類只是簡單以它們對應的整型值作為雜湊碼返回。
· 從JDK 1.3 版本開始,java.lang.String 類緩衝了它的雜湊碼,它只計算一次雜湊碼並且把它存入一個整形變數中,下次調用hashCode方法時,它將直接被返回。這樣做是合法的,因為java.lang.String 被設計為不可變的。
· 在計算一個對象的雜湊碼值時包含一個隨機數是不正確的,因為它在同一次運行中的多次調用不會返回一致的雜湊碼。
複習練習
幾個有用的複習題 – 點這
資源清單
如果你想瞭解更多關於這兩個方法的細節,這份列表能給你提供協助。
· Object class - java.lang.Object 類的API文檔。這兩個方法的常用約定可以在這裡找到。
· Effective Java - Joshua Bloch的一本好書。這本書pdf格式的第三章可以線上擷取。這章涉及了java.lang.Object 類所有的方法。它也詳細討論了equals方法和hashCode方法的實現,正確和不正確的方法等。本文部分內容來源與這本書所提供的資訊。然而,本文試圖通過一些簡潔的例子來解釋這兩個方法的機制和實現細節。並且提供了一些有助於理解的技巧和複習題。
· JavaWorld Article – 這篇文章討論了java.lang.Object 類的相關方法,也解釋了在equals方法中使用 instanceof 條件運算式的缺點。
· Importance of equals and hashCode – 這個常見問題集中談到了正確覆蓋equals方法和hashCode方法的重要性。它也提到了這兩個方法在Java集合架構中的重要地位。
· Equals & Hash Code Mock Test – 如果你對這兩個方法的小測試感興趣的話。
· Equals and Hash Code Cartoon – 我創作的一個連環畫,嘗試使用插圖的方式解釋equals方法和hashCode方法的關係。
· Whizlabs Java Certification (SCJP 1.4) Exam Simulator – 詳細解釋了關於equals方法和hashCode方法的題目,還有其他的一些SCJP 1.4 試題。
致謝 :)
為了使得這篇文章儘可能的準確,它被多次修訂。要感謝Valentin Crettaz 和JavaRanch的朋友們為修改提出的建議。尤其要感謝Valentin,他指出了在equals方法中使用instanceof條件運算式會違反對稱性原則。如果您在拙作中發現了任何含糊不清或者錯誤的地方,請聯絡我。您的回應將有助於提高本文的品質。