Ⅰ 哈希碼的簡介
什麼是哈希碼(HashCode)
在Java中,哈希碼代表對象的特徵。
例如對象 String str1 = 「aa」, str1.hashCode= 3104
String str2 = 「bb」, str2.hashCode= 3106
String str3 = 「aa」, str3.hashCode= 3104
根據HashCode由此可得出str1!=str2,str1==str3
下面給出幾個常用的哈希碼的演算法。
1:Object類的hashCode.返回對象的內存地址經過處理後的結構,由於每個對象的內存地址都不一樣,所以哈希碼也不一樣。
2:String類的hashCode.根據String類包含的字元串的內容,根據一種特殊演算法返回哈希碼,只要字元串所在的堆空間相同,返回的哈希碼也相同。
3:Integer類,返回的哈希碼就是Integer對象里所包含的那個整數的數值,例如Integer i1=new Integer(100),i1.hashCode的值就是100 。由此可見,2個一樣大小的Integer對象,返回的哈希碼也一樣。
Ⅱ 如何正確實現Java中的hashCode方法
正確實現Java中的hashCode方法:
相等和哈希碼
相等是從一般的方面來講,哈希碼更加具有技術性。如果我們在理解方面存在困難,我們可以說,他們通過只是一個實現細節來提高了性能。
大多數的數據結構通過equals方法來判斷他們是否包含一個元素,例如:
List<String> list = Arrays.asList("a", "b", "c");
boolean contains = list.contains("b");
這個變數contains結果是true,因為,雖然」b」是不相同的實例(此外,忽略字元串駐留),但是他們是相等的。
通過比較實例的每個元素,然後將比較結果賦值給contains是比較浪費的,雖然整個類的數據結構進行了優化,能夠提升性能。
他們通過使用一種快捷的方式(減少潛在的實例相等)進行比較,從而代替通過比較實例所包含的每個元素。而快捷比較僅需要比較下面這些方面:
快捷方式比較即通過比較哈希值,它可以將一個實例用一個整數值來代替。哈希碼相同的實例不一定相等,但相等的實例一定具有有相同的哈希值。(或應該有,我們很快就會討論這個)這些數據結構經常通過這種這種技術來命名,可以通過Hash來識別他們的,其中,HashMap是其中最著名的代表。
它們通常是這樣這樣運作的
當添加一個元素,它的哈希碼是用來計算內部數組的索引(即所謂的桶)
如果是,不相等的元素有相同的哈希碼,他們最終在同一個桶上並且捆綁在一起,例如通過添加到列表。
當一個實例來進行contains操作時,它的哈希碼將用來計算桶值(索引值),只有當對應索引值上存在元素時,才會對實例進行比較。
因此equals,hashCode是定義在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是通過多個欄位結合來計算哈希碼的。都是通過Object的hash函數來計算。
選擇欄位
但哪些欄位是相關的嗎?需求將會幫助我們回答這個問題:如果相等的對象必須具有相同的哈希碼,那麼計算哈希碼就不應包括任何不用於相等檢查的欄位。(否則兩個對象只是這些欄位不同但是仍然有可能會相等,此時他們這兩個對象哈希碼卻會不相同。)
所以用於哈希組欄位應該相等時使用的欄位的子集。默認情況下都使用相同的欄位,但有一些細節需要考慮。
一致性
首先,有一致性的要求。它應該相當嚴格。雖然它允許如果一些欄位改變對應的哈希碼發生變化(對於可變的類是不可避免的),但是哈希數據結構並不是為這種場景准備的。
正如我們以上所見的哈希碼用於確定元素的桶。但如果hash-relevant欄位發生了改變,並不會重新計算哈希碼、也不會更新內部數組。
這意味著以後通過相等的對象,甚至同一實例進行查詢也會失敗,數據結構計算當前的哈希碼與之前存儲實例計算的哈希碼並不一致,並是錯誤的桶。
結論:最好不要使用可變欄位計算哈希碼!
性能
哈希碼最終計算的頻率與可能調用equals差不多,那麼這里將是影響性能的關鍵部分,因此考慮此部分性能也是非常有意義的。並且與equals相比,優化之後又更大的上升空間。
除非使用非常復雜的演算法或者涉及非常多的欄位,那麼計算哈希碼的運算成本是微不足道的、同樣也是不可避免的。但是也應該考慮是否需要包含所有的欄位來進行運算。集合需要特別警惕的對待。以Lists和sets為例,將會包含集合裡面的每一個元素來計算哈希碼。是否需要調用它們需要具體情況具體分析。
如果性能是至關重要的,使用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性能,所以除非分析表明必要性,否則不要浪費太多的精力。