1. 程式人生 > >如何正確實現 Java 中的 HashCode

如何正確實現 Java 中的 HashCode

讓我 instance 根據 person 哈希算法 失敗 提高 一起 結果

原文出處: 開源中國

相等 和 Hash Code

從一般角度來看,Equality 是不錯的,但是 hash code 更則具技巧性。如果我們在 hash code上多下點功夫,我們就能了解到 hash code 就是用在細微處去提升性能的。

大部分的數據結構使用equals去檢查是否他們包含一個元素。例如:

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

boolean contains = list.contains("b");

這個變量 contains 是true。因為他們是相等的,雖然b的實例化(instance)雖然不完全一樣(再說一次,忽略String interning)。

將傳遞給 contains 的實例與每個元素進行比較很浪費時間。還好,整個這類數據結構使用了一種更高效的方法。它不會將請求的實例與每個元素比較,而是使用捷徑,找到可能與之相等的實例,然後只比較這幾項。

這個捷徑就是哈希碼——從對象計算出來的一個能代表該對象的整數值。與哈希碼相同的實例不必相等,但相等的實例一定有相同的哈希碼。(或者說應該有,我們稍後會對這個問題進行簡單討論)。這類的數據結構常常使用這種技術命名,在名稱中加入 Hash 以便識別,其中最具代表性的就是 HashMap。

一般情況下它們會這樣進行:

  • 添加一個元素的時候,使用它的哈希碼來計算存放在內部數組(稱為桶)中的位置(序號)。
  • 另一個不等同的元素如果具有相同的哈希碼,它會被放在同一個桶中,與原來那個放在一起,比如把它們放在一個列表中。
  • 如果傳遞一個實例給 contains 方法,會先計算它的哈希碼來找到桶,只有同一個桶中的元素需要與這個實例進行比較。

使用這種方法實現 contains 的情況很少,在理想的狀態下根本不需要 equals 比較。

將 equals、hashCode 定義在 Object 中。

關於哈希的一些思考

如果把 hashCode 作為一種快捷方式取決於其是否相等,那麽只有一件事情我們需要關心:相等的對象應該有一致的哈希碼。

這也是為什麽,如果我們覆寫 equals 方法,就必須創建一個匹配的 hashCode 實現!此外,實現 equal 應該是依據我們的實現而實現的,這可能會導致沒有相同的哈希碼,因為他們使用的是 Object 的實現。

hashCode 約定

從原文檔引用:

對於 hashCode 的一般約定:

  • 在 Java 應用程序中,任何時候對同一對象多次調用 hashCode 方法,都必須一直返回同樣的整數,對它提供的信息也用於對象的相等比較,且不會被修改。這個整數在兩次對同一個應用程序的執行中不需要保持一致。
  • 如果兩個對象通過 equals(Object) 方法來比較結果相等,那麽這兩個對象的 hashCode 方法必須產生同樣的整型結果。
  • 如果兩個對象通過 equals(Object) 方法來比較結果不等,這兩個對象的 hashCode 不必產生不同整型結果。然而,開發者應該了解對不等的對象產生不同的整型結果有助於提高哈希表的性能。

第一條反映了 equals 的一致性。第二條是我們在上面提到的要求。第三條陳述了我們下面要討論的一個重要細節。

實現 hashCode

Person.hashCode 有個很簡單的實現:

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

通過計算相關字段的哈希碼,再把這些哈希碼組合起來得到 person 的哈希碼。它們用 Object 的工具函數 hash 來參與計算。

選擇字段

然而什麽字段才是相關的?這些要求有助於回答這個問題:如果相等的對象必須有相同的哈希碼,那麽在計算哈希碼的時候就不應該使用那些不用於相等性檢查的字段。(否則,如果兩個對象只有那些字段不同的話,它們會相等但哈希碼不同。)

所以用於計算哈希碼的那些字段應該是用於相等性比較的那些字段的子集。默認情況下,它們會使用相同的字段,但有幾個細節需要考慮。

一致性

第一是一致性要求。它應該經過非常嚴格的計算。如果有字段產生了變化,哈希碼也應該允許變化(對於可變類來說,這往往是不可避免的),依賴哈希的數據結構並未準備應付這種情況。

正如我們在上面看到的那樣,哈希碼用於確定一個元素的桶,但是如果哈希相關的字段發生變化,並不會立即重新計算哈希碼,而且內部的數組也不會更新。

這就意味著,再對一個相等的對象甚至同一個對象的查詢會失敗!這個數據結構會計算當前的哈希碼,這個哈希碼與實例存入時的哈希碼並不相同,這直接導致找錯了桶。

小結:最好不要用可變的字段來計算哈希碼!

性能

哈希碼可能最終會在每次調用 equals 的時候計算,這可能正好發生在代碼中性能極為關鍵的部分,所以考慮性能是很有意義的。相比之下 equals 的優化空間就非常小。

除非是使用了復雜的算法,或者使用的字段非常非常多,組合他們哈希碼的計算成本可以忽略不計,因為這不可避免。但是應該考慮是否所有字段都需要包含在計算中!尤其應該以審視的眼光來看待集合,例如計算列表和集合中所有元素的哈希碼。需要根據不同的情況來考慮是否需要它們參與計算。

如果性能是關鍵,使用 Object.hash 就可能不是最好的選擇,因為它會為可變參數創建數組。

一般的優化原則是:謹慎處理!使用一個公共哈希算法的,可能需要放棄集合,並在分析可能的改進之後進行優化。

碰撞

如果只關註性能,下面這個實例怎麽樣?

@Override

public int hashCode() {

return 0;

}

毫無疑問,它很快。而且相等的對象會有相同的哈希碼,這也讓我們覺得不錯。還有個亮點,它不涉及可變的字段!

但是,想想我們提到的桶是什麽?這種情況下所有實例會被裝進同一個桶中!通常這會導致使用一個鏈表來容納所有元素,這樣的性能太糟糕了——比如,每次執行 contains 都會對列表進行線性掃描。

因此,我們得讓每個桶裏的內容盡可能的少!一個即使對非常相似的對象計算的哈希碼也大不相同的算法,會是一個不錯的開始。

如何取得,一定程度上取決於選擇的字段。我們用於計算的細節,更多時候是為了生成不同的哈希碼。註意,這與我們對性能的想法完全相反。結果很有趣,用太多或者太少字段都會導致性能不佳。

防止碰撞的算法是哈希算法的另一部分。

計算哈希

計算字段的哈希碼最簡單的辦法就是直接調用這個字段的 `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。就會發生大量的碰撞!

如何正確實現 Java 中的 HashCode