- 相關(guān)推薦
如何正確實(shí)現(xiàn)Java中的hashCode方法
導(dǎo)語:hashCode是jdk根據(jù)對象的地址或者字符串或者數(shù)字算出來的int類型的數(shù)值,那么如何正確實(shí)現(xiàn)Java中的hashCode方法呢?一起來學(xué)習(xí)下吧:
相等和哈希碼
相等是從一般的方面來講,哈希碼更加具有技術(shù)性。如果我們在理解方面存在困難,我們可以說,他們通過只是一個(gè)實(shí)現(xiàn)細(xì)節(jié)來提高了性能。
大多數(shù)的數(shù)據(jù)結(jié)構(gòu)通過equals方法來判斷他們是否包含一個(gè)元素,例如:
List
boolean contains = list.contains("b");
這個(gè)變量contains結(jié)果是true,因?yàn)?雖然”b”是不相同的實(shí)例(此外,忽略字符串駐留),但是他們是相等的。
通過比較實(shí)例的每個(gè)元素,然后將比較結(jié)果賦值給contains是比較浪費(fèi)的,雖然整個(gè)類的數(shù)據(jù)結(jié)構(gòu)進(jìn)行了優(yōu)化,能夠提升性能。
他們通過使用一種快捷的方式(減少潛在的實(shí)例相等)進(jìn)行比較,從而代替通過比較實(shí)例所包含的每個(gè)元素。而快捷比較僅需要比較下面這些方面:
快捷方式比較即通過比較哈希值,它可以將一個(gè)實(shí)例用一個(gè)整數(shù)值來代替。哈希碼相同的實(shí)例不一定相等,但相等的實(shí)例一定具有有相同的哈希值。(或應(yīng)該有,我們很快就會討論這個(gè))這些數(shù)據(jù)結(jié)構(gòu)經(jīng)常通過這種這種技術(shù)來命名,可以通過Hash來識別他們的,其中,HashMap是其中最著名的代表。
它們通常是這樣這樣運(yùn)作的
當(dāng)添加一個(gè)元素,它的哈希碼是用來計(jì)算內(nèi)部數(shù)組的索引(即所謂的桶)
如果是,不相等的元素有相同的哈希碼,他們最終在同一個(gè)桶上并且捆綁在一起,例如通過添加到列表。
當(dāng)一個(gè)實(shí)例來進(jìn)行contains操作時(shí),它的哈希碼將用來計(jì)算桶值(索引值),只有當(dāng)對應(yīng)索引值上存在元素時(shí),才會對實(shí)例進(jìn)行比較。
因此equals,hashCode是定義在Object類中。
散列法的思想
如果hashCode作為快捷方式來確定相等,那么只有一件事我們應(yīng)該關(guān)心:相等的對象應(yīng)該具有相同的哈希碼,這也是為什么如果我們重寫了equals方法后,我們必須創(chuàng)建一個(gè)與之匹配的hashCode實(shí)現(xiàn)的原因!
否則相等的對象是可能不會有相同的哈希碼的,因?yàn)樗鼈儗⒄{(diào)用的是Object's的默認(rèn)實(shí)現(xiàn)。
HashCode 準(zhǔn)則
引用自官方文檔
hashCode通用約定:
* 調(diào)用運(yùn)行Java應(yīng)用程序中的同一對象,hashCode方法必須始終返回相同的整數(shù)。這個(gè)整數(shù)不需要在不同的Java應(yīng)用程序中保持一致。
* 根據(jù)equals(Object)的方法來比較,如果兩個(gè)對象是相等的,兩個(gè)對象調(diào)用hashCode方法必須產(chǎn)生相同的結(jié)果。
* 根據(jù)equals(Object)的方法是比較,如果兩個(gè)對象是不相等的,那么兩個(gè)對象調(diào)用hashCode方法并不一定產(chǎn)生不同的整數(shù)的結(jié)果。但是,程序員應(yīng)該意識到給不相等的對象產(chǎn)生不同的整數(shù)結(jié)果將有可能提高哈希表的性能。
第一點(diǎn)反映出了相等的一致性屬性,第二個(gè)就是我們上面提出的要求。第三個(gè)闡述了一個(gè)重要的細(xì)節(jié),我們將在稍后討論。
HashCode實(shí)現(xiàn)
下面是非常簡單的Person.hashCode的實(shí)現(xiàn)
@Override
public int hashCode() {
return Objects.hash(firstName, lastName);
}
person’s是通過多個(gè)字段結(jié)合來計(jì)算哈希碼的。都是通過Object的hash函數(shù)來計(jì)算。
選擇字段
但哪些字段是相關(guān)的嗎?需求將會幫助我們回答這個(gè)問題:如果相等的對象必須具有相同的哈希碼,那么計(jì)算哈希碼就不應(yīng)包括任何不用于相等檢查的字段。(否則兩個(gè)對象只是這些字段不同但是仍然有可能會相等,此時(shí)他們這兩個(gè)對象哈希碼卻會不相同。)
所以用于哈希組字段應(yīng)該相等時(shí)使用的字段的子集。默認(rèn)情況下都使用相同的字段,但有一些細(xì)節(jié)需要考慮。
一致性
首先,有一致性的要求。它應(yīng)該相當(dāng)嚴(yán)格。雖然它允許如果一些字段改變對應(yīng)的哈希碼發(fā)生變化(對于可變的類是不可避免的),但是哈希數(shù)據(jù)結(jié)構(gòu)并不是為這種場景準(zhǔn)備的。
正如我們以上所見的哈希碼用于確定元素的桶。但如果hash-relevant字段發(fā)生了改變,并不會重新計(jì)算哈希碼、也不會更新內(nèi)部數(shù)組。
這意味著以后通過相等的對象,甚至同一實(shí)例進(jìn)行查詢也會失敗,數(shù)據(jù)結(jié)構(gòu)計(jì)算當(dāng)前的哈希碼與之前存儲實(shí)例計(jì)算的哈希碼并不一致,并是錯(cuò)誤的桶。
結(jié)論:最好不要使用可變字段計(jì)算哈希碼!
性能
哈希碼最終計(jì)算的頻率與可能調(diào)用equals差不多,那么這里將是影響性能的關(guān)鍵部分,因此考慮此部分性能也是非常有意義的。并且與equals相比,優(yōu)化之后又更大的上升空間。
除非使用非常復(fù)雜的算法或者涉及非常多的字段,那么計(jì)算哈希碼的運(yùn)算成本是微不足道的、同樣也是不可避免的。但是也應(yīng)該考慮是否需要包含所有的字段來進(jìn)行運(yùn)算。集合需要特別警惕的對待。以Lists和sets為例,將會包含集合里面的每一個(gè)元素來計(jì)算哈希碼。是否需要調(diào)用它們需要具體情況具體分析。
如果性能是至關(guān)重要的,使用Objects.hash因?yàn)樾枰獮関arargs創(chuàng)建一個(gè)數(shù)組也許并不是最好的選擇。但一般規(guī)則優(yōu)化是適用的:不要過早地使用一個(gè)通用的散列碼算法,也許需要放棄集合,只有優(yōu)化分析顯示潛在的改進(jìn)。
碰撞
總是關(guān)注性能,這個(gè)實(shí)現(xiàn)怎么呢?
@Override
public int hashCode() {
return 0;
}
快是肯定的。相等的對象將具有相同的哈希碼。并且,沒有可變的字段!
但是,我們之前說過的桶呢?!這種方式下所有的實(shí)例將會有相同的桶!這將會導(dǎo)致一個(gè)鏈表來包含所有的元素,這樣一來將會有非常差的性能。每次調(diào)用contains將會觸發(fā)對整個(gè)list線性掃描。
我們希望盡可能少的元素在同一個(gè)桶!一個(gè)算法返回變化多端的哈希碼,即使對于非常相似的對象,是一個(gè)好的開始。
怎樣才能達(dá)到上面的效果部分取決于選取的字段,我們在計(jì)算中包含更多的細(xì)節(jié),越有可能獲取到不同的哈希碼。注意:這個(gè)與我們所說的性能是完全相反的。因此,有趣的是,使用過多或者過少的字段都會導(dǎo)致糟糕的性能。
防止碰撞的另一部分是使用實(shí)際計(jì)算散列的算法。
計(jì)算Hsah
最簡單的方法來計(jì)算一個(gè)字段的哈希碼是通過直接調(diào)用hashCode,結(jié)合的話會自動完成。常見的算法是首先在以任意數(shù)量的數(shù)值(通常是基本數(shù)據(jù)類型)反復(fù)進(jìn)行相乘操作再與字段哈希碼相加
int prime = 31;
int result = 1;
result = prime * result + ((firstName == null) ? 0 : firstName.hashCode());
result = prime * result + ((lastName == null) ? 0 : lastName.hashCode());
return result;
這可能導(dǎo)致溢出,但是不是特別有問題的,因?yàn)樗麄儾]有產(chǎn)生Java異常。
注意,即使是非常良好的的哈希算法也可能因?yàn)檩斎胩囟ǖ哪J降臄?shù)據(jù)有導(dǎo)致頻繁碰撞。作為一個(gè)簡單的例子假設(shè)我們會計(jì)算點(diǎn)的散列通過增加他們的x和y坐標(biāo)。當(dāng)我們處理f(x) = -x線上的點(diǎn)時(shí),線上的點(diǎn)都滿足:x + y == 0,將會有大量的碰撞。
但是:我們可以使用一個(gè)通用的算法,只到分析表明并不正確,才需要對哈希算法進(jìn)行修改。
總結(jié)
我們了解到計(jì)算哈希碼就是壓縮相等的一個(gè)整數(shù)值:相等的對象必須有相同的哈希碼,而出于對性能的考慮:最好是盡可能少的不相等的對象共享相同的哈希碼。
這就意味著如果重寫了equals方法,那么就必須重寫hashCode方法
當(dāng)實(shí)現(xiàn)hashCode
使用與equals中使用的相同的字段(或者equals中使用字段的子集)
最好不要包含可變的字段。
對集合不要考慮調(diào)用hashCode
如果沒有特殊的輸入特定的模式,盡量采用通用的哈希算法
記住hashCode性能,所以除非分析表明必要性,否則不要浪費(fèi)太多的精力。
【如何正確實(shí)現(xiàn)Java中的hashCode方法】相關(guān)文章:
java中的hashCode小例子教程05-23
Java中如何實(shí)現(xiàn)顯示動態(tài)的時(shí)間03-14
如何在java中實(shí)現(xiàn)左右鍵菜單03-20
關(guān)于Java動態(tài)實(shí)現(xiàn)的方法04-20
JAVA實(shí)現(xiàn)生成GUID的方法06-02
如何正確使用Java數(shù)組04-29