在每個(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è)名為result
的int
類型的變量中姥闪。
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
冯吓、Integer
、Date
等都可以把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ù)返回的確切值。