如何正確實現Java中的hashCode方法

2016-06-22    分類:JAVA開發、程式設計開發、首頁精華0人評論發表於2016-06-22

本文由碼農網 – 漠北空城原創翻譯,轉載請看清文末的轉載要求,歡迎參與我們的付費投稿計劃

你知道一個物件的唯一標誌不能僅僅通過寫一個漂亮的equals來實現

太棒了,不過現在你也必須實現hashCode方法。

讓我們看看為什麼和怎麼做才是正確的。

相等和雜湊碼

相等是從一般的方面來講,雜湊碼更加具有技術性。如果我們在理解方面存在困難,我們可以說,他們通過只是一個實現細節來提高了效能。

大多數的資料結構通過equals方法來判斷他們是否包含一個元素,例如:

List<String> list = Arrays.asList("a", "b", "c");
boolean contains = list.contains("b");

這個變數contains結果是true,因為,雖然”b”是不相同的例項(此外,忽略字串駐留),但是他們是相等的。

通過比較例項的每個元素,然後將比較結果賦值給contains是比較浪費的,雖然整個類的資料結構進行了優化,能夠提升效能。

他們通過使用一種快捷的方式(減少潛在的例項相等)進行比較,從而代替通過比較例項所包含的每個元素。而快捷比較僅需要比較下面這些方面:

快捷方式比較即通過比較雜湊值,它可以將一個例項用一個整數值來代替。雜湊碼相同的例項不一定相等,但相等的例項一定具有有相同的雜湊值。(或應該有,我們很快就會討論這個)這些資料結構經常通過這種這種技術來命名,可以通過Hash來識別他們的,其中,HashMap是其中最著名的代表。

它們通常是這樣這樣運作的

  • 當新增一個元素,它的雜湊碼是用來計算內部陣列的索引(即所謂的桶)
  • 如果是,不相等的元素有相同的雜湊碼,他們最終在同一個桶上並且捆綁在一起,例如通過新增到列表。
  • 當一個例項來進行contains操作時,它的雜湊碼將用來計算桶值(索引值),只有當對應索引值上存在元素時,才會對例項進行比較。

因此equalshashCode是定義在Object類中。

雜湊法的思想

如果hashCode作為快捷方式來確定相等,那麼只有一件事我們應該關心:相等的物件應該具有相同的雜湊碼,這也是為什麼如果我們重寫了equals方法後,我們必須建立一個與之匹配的hashCode實現的原因!

否則相等的物件是可能不會有相同的雜湊碼的,因為它們將呼叫的是Object's的預設實現。

HashCode 準則

引用自官方文件

hashCode通用約定:
* 呼叫執行Java應用程式中的同一物件,hashCode方法必須始終返回相同的整數。這個整數不需要在不同的Java應用程式中保持一致。
* 根據equals(Object)的方法來比較,如果兩個物件是相等的,兩個物件呼叫hashCode方法必須產生相同的結果。
* 根據equals(Object)的方法是比較,如果兩個物件是不相等的,那麼兩個物件呼叫hashCode方法並不一定產生不同的整數的結果。但是,程式設計師應該意識到給不相等的物件產生不同的整數結果將有可能提高雜湊表的效能。

第一點反映出了相等的一致性屬性,第二個就是我們上面提出的要求。第三個闡述了一個重要的細節,我們將在稍後討論。

HashCode實現

下面是非常簡單的Person.hashCode的實現

@Override
public int hashCode() {
    return Objects.hash(firstName, lastName);
}

person’s是通過多個欄位結合來計算雜湊碼的。都是通過Objecthash函式來計算。

選擇欄位

但哪些欄位是相關的嗎?需求將會幫助我們回答這個問題:如果相等的物件必須具有相同的雜湊碼,那麼計算雜湊碼就不應包括任何不用於相等檢查的欄位。(否則兩個物件只是這些欄位不同但是仍然有可能會相等,此時他們這兩個物件雜湊碼卻會不相同。)

所以用於雜湊組欄位應該相等時使用的欄位的子集。預設情況下都使用相同的欄位,但有一些細節需要考慮。

一致性

首先,有一致性的要求。它應該相當嚴格。雖然它允許如果一些欄位改變對應的雜湊碼發生變化(對於可變的類是不可避免的),但是雜湊資料結構並不是為這種場景準備的。

正如我們以上所見的雜湊碼用於確定元素的桶。但如果hash-relevant欄位發生了改變,並不會重新計算雜湊碼、也不會更新內部陣列。

這意味著以後通過相等的物件,甚至同一例項進行查詢也會失敗,資料結構計算當前的雜湊碼與之前儲存例項計算的雜湊碼並不一致,並是錯誤的桶。

結論:最好不要使用可變欄位計算雜湊碼!

效能

雜湊碼最終計算的頻率與可能呼叫equals差不多,那麼這裡將是影響效能的關鍵部分,因此考慮此部分效能也是非常有意義的。並且與equals相比,優化之後又更大的上升空間。

除非使用非常複雜的演算法或者涉及非常多的欄位,那麼計算雜湊碼的運算成本是微不足道的、同樣也是不可避免的。但是也應該考慮是否需要包含所有的欄位來進行運算。集合需要特別警惕的對待。以Listssets為例,將會包含集合裡面的每一個元素來計算雜湊碼。是否需要呼叫它們需要具體情況具體分析。

如果效能是至關重要的,使用Objects.hash因為需要為varargs建立一個陣列也許並不是最好的選擇。但一般規則優化是適用的:不要過早地使用一個通用的雜湊碼演算法,也許需要放棄集合,只有優化分析顯示潛在的改進。

碰撞

總是關注效能,這個實現怎麼呢?

@Override
public int hashCode() {
    return 0;
}

快是肯定的。相等的物件將具有相同的雜湊碼。並且,沒有可變的欄位!

但是,我們之前說過的桶呢?!這種方式下所有的例項將會有相同的桶!這將會導致一個連結串列來包含所有的元素,這樣一來將會有非常差的效能。每次呼叫contains將會觸發對整個list線性掃描。

我們希望儘可能少的元素在同一個桶!一個演算法返回變化多端的雜湊碼,即使對於非常相似的物件,是一個好的開始。

怎樣才能達到上面的效果部分取決於選取的欄位,我們在計算中包含更多的細節,越有可能獲取到不同的雜湊碼。注意:這個與我們所說的效能是完全相反的。因此,有趣的是,使用過多或者過少的欄位都會導致糟糕的效能。

防止碰撞的另一部分是使用實際計算雜湊的演算法。

計算Hsah

最簡單的方法來計算一個欄位的雜湊碼是通過直接呼叫hashCode,結合的話會自動完成。常見的演算法是首先在以任意數量的數值(通常是基本資料型別)反覆進行相乘操作再與欄位雜湊碼相加

int prime = 31;
int result = 1;
result = prime * result + ((firstName == null) ? 0 : firstName.hashCode());
result = prime * result + ((lastName == null) ? 0 : lastName.hashCode());
return result;

這可能導致溢位,但是不是特別有問題的,因為他們並沒有產生Java異常。

注意,即使是非常良好的的雜湊演算法也可能因為輸入特定的模式的資料有導致頻繁碰撞。作為一個簡單的例子假設我們會計算點的雜湊通過增加他們的x和y座標。當我們處理f(x) = -x線上的點時,線上的點都滿足:x + y == 0,將會有大量的碰撞。

但是:我們可以使用一個通用的演算法,只到分析表明並不正確,才需要對雜湊演算法進行修改。

總結

我們瞭解到計算雜湊碼就是壓縮相等的一個整數值:相等的物件必須有相同的雜湊碼,而出於對效能的考慮:最好是儘可能少的不相等的物件共享相同的雜湊碼。

這就意味著如果重寫了equals方法,那麼就必須重寫hashCode方法

當實現hashCode

  • 使用與equals中使用的相同的欄位(或者equals中使用欄位的子集)
  • 最好不要包含可變的欄位。
  • 對集合不要考慮呼叫hashCode
  • 如果沒有特殊的輸入特定的模式,儘量採用通用的雜湊演算法

記住hashCode效能,所以除非分析表明必要性,否則不要浪費太多的精力。

譯文連結:http://www.codeceo.com/article/java-hashcode-implement.html
英文原文:How to Implement Java’s hashCode Correctly
翻譯作者:碼農網 – 漠北空城
轉載必須在正文中標註並保留原文連結、譯文連結和譯者等資訊。]

相關文章