高效Java第九條覆蓋equals時(shí)總要覆蓋hashCode

在每個(gè)覆蓋了equals方法的類中,也必須覆蓋hashCode方法更啄。
否則會(huì)導(dǎo)致該類無法與基于散列的集合一起正常運(yùn)作纳令。

hashCode約定

  • 在應(yīng)用程序的執(zhí)行期間柠贤,只要對象的equals方法所用到的信息沒有被修改帮孔,那么對著同一個(gè)對象調(diào)用多次雷滋,hashCode方法都必須始終如一地返回同一個(gè)整數(shù)不撑。
  • 如果兩個(gè)對象equals(Object)方法比較是相等的,那么調(diào)用這兩個(gè)對象的hashCode方法必須產(chǎn)生同樣的整數(shù)結(jié)果晤斩。
  • 如果兩個(gè)對象equals(Object)比較是不相等的焕檬,那么調(diào)用這兩個(gè)對象中的hashCode方法,則不一定要產(chǎn)生不同的整數(shù)結(jié)果澳泵。但是給不相等的對象產(chǎn)生截然不同的整數(shù)結(jié)果实愚,有可能提高散列表的性能。

沒有覆蓋hashCode違反第二條約定

因沒有覆蓋hashCode而違反的關(guān)鍵約定是第二條:相等的對象必須具有相等的散列碼兔辅。

PhoneNumber例子——沒有覆蓋hashCode

該類無法與HashMap一起正常工作:

m.get(new PhoneNumber(408, 867, 5309))返回null腊敲。
由于PhoneNumber類沒有覆蓋hashCode方法,從而導(dǎo)致兩個(gè)相等的實(shí)例具有不相等的散列碼幢妄,違反了hashCode的約定兔仰。因此,put方法把電話號碼對象存放在一個(gè)散列桶中蕉鸳,get方法卻在另一個(gè)散列桶中查找這個(gè)電話號碼。即使這兩個(gè)實(shí)例正好被放到同一個(gè)散列桶中忍法,get方法也必定會(huì)返回null潮尝,因?yàn)?code>HahsMsap有一項(xiàng)優(yōu)化,可以將每個(gè)項(xiàng)相關(guān)聯(lián)的散列碼緩存起來饿序,如果散列碼不匹配勉失,也不必檢驗(yàn)對象的等同性。

PhoneNumber類提供一個(gè)適當(dāng)?shù)?code>hashCode方法原探。
下面的hashCode方法是錯(cuò)誤的:

雖然上面的hashCode方法確保了相等的對象總是具有同樣的散列碼乱凿。但是它使得每個(gè)對象都具有同樣的散列碼。因此咽弦,每個(gè)對象都被映射到同一個(gè)散列桶中徒蟆,使散列表退化為鏈表。它使得本該線性時(shí)間運(yùn)行的程序變成了以平方級時(shí)間在運(yùn)行型型。對于規(guī)模很大的散列表而言段审,這關(guān)系到散列表能否正常工作。

如何編寫好的散列函數(shù)

一個(gè)好的散列函數(shù)傾向于為不相等的對象產(chǎn)生不相等的散列碼闹蒜。
散列函數(shù)應(yīng)該把集合中不相等的實(shí)例均勻地分布到所有可能的散列值上寺枉。

1.把某個(gè)非零的常數(shù)值,比如17绷落,保存在一個(gè)名為resultint類型的變量中姥闪。
2.對于對象中每個(gè)關(guān)鍵域f(指equals方法涉及的每個(gè)域),完成以下步驟:
a砌烁。為該域計(jì)算int類型的散列碼c:
i.如果該域是boolean類型筐喳,則計(jì)算(f?1:0)。
ii.如果該域是byte疏唾、char蓄氧、short或者int類型,則計(jì)算(int)f槐脏。
iii.如果該域是long類型喉童,則計(jì)算(int)(f ^ (f >>> 32))
iv.如果該域是float類型顿天,則計(jì)算Float.floatToIntBits(f)堂氯。
v.如果該域是double類型,則計(jì)算Double.doubleToLongBits(f)牌废,然后按照步驟2.a.iii咽白,為得到的long類型值計(jì)算散列值。
vi.如果該域是一個(gè)對象引用鸟缕,并且該類的equals方法通過遞歸地調(diào)用equals方法來比較這個(gè)域晶框,則同樣為這個(gè)域遞歸地調(diào)用hashCode。如果需要更復(fù)雜的比較懂从,則為這個(gè)域計(jì)算一個(gè)范式授段,然后針對這個(gè)范式調(diào)用hashCode。如果這個(gè)域的值為null番甩,則返回0(或者其他某個(gè)常數(shù)侵贵,但通常是0)。

vii.如果該域是一個(gè)數(shù)組缘薛,則要把每一個(gè)元素當(dāng)做單獨(dú)的域來處理窍育。遞歸地應(yīng)用上述規(guī)則,對每個(gè)重要的元素計(jì)算一個(gè)散列碼宴胧,然后根據(jù)步驟2.b中的做法把這些散列值組合起來漱抓。如果數(shù)組域中的每個(gè)元素都很重要,建議使用Arrays.hashCode方法牺汤。
b.把步驟2.a中計(jì)算得到的散列碼c合并到result中:
result = 31 * result + c;
3.返回result辽旋。

4.寫完了hashCode方法之后,問問自己“相等的實(shí)例是否都具有相等的散列碼”檐迟。建議編寫單元測試补胚。

計(jì)算散列碼可以把冗余域排除在外。如果一個(gè)域的值可以根據(jù)參與計(jì)算的其他域的值計(jì)算出來追迟,則可以把這樣的域排除在外溶其。必須排除equals比較計(jì)算中沒有用到的任何域,否則很有可能違反hashCode約定的第二條敦间。

值17是任選的瓶逃。
步驟2.b中的乘法部分使得散列值依賴于域的順序束铭,如果一個(gè)類中包含多個(gè)相似的域截歉,這樣的乘法運(yùn)算就會(huì)產(chǎn)生一個(gè)更好的散列函數(shù)哼勇。String的散列函數(shù)省略了乘法部分,那么只是字母順序不同的所有字符串就會(huì)有相同的散列碼吼畏。
選擇31是因?yàn)樗且粋€(gè)奇素?cái)?shù)昔汉。如果乘數(shù)是偶數(shù)懈万,并且乘法溢出的話,信息就會(huì)丟失靶病,因?yàn)榕c2相乘等價(jià)于移位運(yùn)算会通。使用素?cái)?shù)的好處并不很明顯,但習(xí)慣上都使用素?cái)?shù)來計(jì)算散列結(jié)果娄周。31可以利用移位和減法來代替乘法涕侈,可以得到更好的性能:31 * i == (i << 5) -i。JVM可以自動(dòng)完成這種優(yōu)化煤辨。

PhoneNumber編寫好的hashCode

PhoneNumber類的hashCode

緩存散列碼

不可變類如果計(jì)算散列碼的開銷比較大裳涛,就應(yīng)該考慮把散列碼緩存在對象內(nèi)部,而不是每次請求的時(shí)候都重新計(jì)算散列碼众辨。
如果類的大多數(shù)對象都會(huì)被用做散列鍵调违,就應(yīng)該在創(chuàng)建實(shí)例的時(shí)候計(jì)算散列碼。否則泻轰,可以選擇“延遲初始化”散列碼,一直到hashCode被第一次調(diào)用的時(shí)候才初始化且轨。
PhoneNumber類的對象有可能會(huì)經(jīng)常被作為散列鍵:

不要排除對象的關(guān)鍵部分

不要試圖從散列碼計(jì)算中排除掉一個(gè)對象的關(guān)鍵部分來提高性能浮声。即使這樣得到的散列函數(shù)運(yùn)行起來更快,但是它的效果不見得會(huì)好旋奢,可能會(huì)導(dǎo)致散列表慢到根本無法使用泳挥。
在實(shí)踐中,散列函數(shù)可能面臨大量的實(shí)例至朗,在選擇忽略的區(qū)域之中屉符,這些實(shí)例區(qū)別非常大。散列函數(shù)會(huì)把所有這些實(shí)例映射到極少數(shù)的散列碼上锹引,基于散列的集合將會(huì)顯示出平方級的性能指標(biāo)矗钟。
JDK2之前的String散列函數(shù)至多只檢查16個(gè)字符,從第一個(gè)字符開始嫌变,在整個(gè)字符串中均勻選取吨艇。對于大型集合,該散列函數(shù)表現(xiàn)出了病態(tài)行為腾啥。

不要規(guī)定散列函數(shù)的細(xì)節(jié)

Java平臺類庫中的許多類东涡,String冯吓、IntegerDate等都可以把hashCode方法返回的確切值規(guī)定為該實(shí)例值的一個(gè)函數(shù)疮跑。但是這并不是一個(gè)好注意,這會(huì)嚴(yán)格地限制了在將來的版本中改進(jìn)散列函數(shù)的能力组贺。如果沒有規(guī)定散列函數(shù)的細(xì)節(jié),那么當(dāng)你發(fā)現(xiàn)了它的內(nèi)部缺陷時(shí)祖娘,就可以在后面的發(fā)行版本中修正它失尖,確信沒有任何客戶端會(huì)依賴于散列函數(shù)返回的確切值。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末贿条,一起剝皮案震驚了整個(gè)濱河市雹仿,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌整以,老刑警劉巖胧辽,帶你破解...
    沈念sama閱讀 223,126評論 6 520
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異公黑,居然都是意外死亡邑商,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,421評論 3 400
  • 文/潘曉璐 我一進(jìn)店門凡蚜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來人断,“玉大人,你說我怎么就攤上這事朝蜘《衤酰” “怎么了?”我有些...
    開封第一講書人閱讀 169,941評論 0 366
  • 文/不壞的土叔 我叫張陵谱醇,是天一觀的道長暇仲。 經(jīng)常有香客問我,道長副渴,這世上最難降的妖魔是什么奈附? 我笑而不...
    開封第一講書人閱讀 60,294評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮煮剧,結(jié)果婚禮上斥滤,老公的妹妹穿的比我還像新娘。我一直安慰自己勉盅,他們只是感情好佑颇,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,295評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著菇篡,像睡著了一般漩符。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上驱还,一...
    開封第一講書人閱讀 52,874評論 1 314
  • 那天嗜暴,我揣著相機(jī)與錄音凸克,去河邊找鬼。 笑死闷沥,一個(gè)胖子當(dāng)著我的面吹牛萎战,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播舆逃,決...
    沈念sama閱讀 41,285評論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼蚂维,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了路狮?” 一聲冷哼從身側(cè)響起虫啥,我...
    開封第一講書人閱讀 40,249評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎奄妨,沒想到半個(gè)月后涂籽,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,760評論 1 321
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡砸抛,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,840評論 3 343
  • 正文 我和宋清朗相戀三年评雌,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片直焙。...
    茶點(diǎn)故事閱讀 40,973評論 1 354
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡景东,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出奔誓,到底是詐尸還是另有隱情斤吐,我是刑警寧澤,帶...
    沈念sama閱讀 36,631評論 5 351
  • 正文 年R本政府宣布厨喂,位于F島的核電站曲初,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏杯聚。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,315評論 3 336
  • 文/蒙蒙 一抒痒、第九天 我趴在偏房一處隱蔽的房頂上張望幌绍。 院中可真熱鬧,春花似錦故响、人聲如沸傀广。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,797評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽伪冰。三九已至,卻和暖如春樟蠕,著一層夾襖步出監(jiān)牢的瞬間贮聂,已是汗流浹背靠柑。 一陣腳步聲響...
    開封第一講書人閱讀 33,926評論 1 275
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留吓懈,地道東北人歼冰。 一個(gè)月前我還...
    沈念sama閱讀 49,431評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像耻警,于是被迫代替她去往敵國和親隔嫡。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,982評論 2 361

推薦閱讀更多精彩內(nèi)容