第9条:覆盖equals时总要覆盖hashCode
在每個覆蓋equals方法的類中,也必須覆蓋hashCode方法。否則,會違反Object.hashCode的通用約定,從而導致該類無法結合所有基于散列的集合一起正常工作,包括HashMap,HashSet,Hashtbale。
?
hashCode約定內容:
1.只要對象equals方法的比較操作所用到的信息沒有被修改,對同一對象調用多次,hashCode方法都必須返回同一整數。在同一應用程序的多次執行過程中,每次執行返回的整數可以不一致。
2.如果兩個對象根據equals(Object)方法比較是相等的,那么這兩個對象的hashCode返回值相同。
3.如果兩個對象根據equals(Object)方法比較是不等的,那么這兩個對象的hashCode返回值不一定不等,但是給不同的對象產生截然不同的整數結果,能提高散列表的性能。
?
考慮:
public class PhoneNumber {private final int areaCode;private final int prefix;private final int lineNumber;public PhoneNumber(int areaCode, int prefix, int lineNumber) {rangeCheck(areaCode, 999, "area code");rangeCheck(prefix, 999, "prefix");rangeCheck(lineNumber, 9999, "line number");this.areaCode = areaCode;this.prefix = prefix;this.lineNumber = lineNumber;}private static void rangeCheck(int arg, int max, String name) {if(arg < 0 || arg > max) {throw new IllegalArgumentException(name + ": " + arg);}}@Overridepublic boolean equals(Object o) {if(o == this)return true;if(!(o instanceof PhoneNumber))return false;PhoneNumber pn = (PhoneNumber)o;return pn.lineNumber == lineNumber&& pn.prefix == prefix&& pn.areaCode == areaCode;}}運行下面代碼:
Map<PhoneNumber, String> map = new HashMap<PhoneNumber, String>(); map.put(new PhoneNumber(707, 867, 5309), "Jenny"); System.out.println(map.get(new PhoneNumber(707, 867, 5309)));我們期望它返回Jenny,然而它返回的是null。
原因在于違反了hashCode的約定,由于PhoneNumber沒有覆蓋hashCode方法,導致兩個相等的實例擁有不相等的散列碼,put方法把電話號碼對象放在一個散列桶中,get方法從另外一個散列桶中查找這個電話號碼的所有者,顯然是無法找到的。
只要覆蓋hashCode并遵守約定,就能修正這個問題。
?
一個好的散列函數傾向于“為不相等的對象產生不相等的散列碼”,下面有簡單的解決辦法:
1.把某個非零的常數值,如17,保存在一個名為result的int類型的變量中。(為了2.a中計算的散列值為0的初始域會影響到散列值)
2.對于對象中的每個關鍵域f,完成一下步驟:
a.為該域計算int類型的散列碼c
i.如果該域是boolean,計算(f ? 1:0)
ii.如果該域是byte、char、short或者int類型,則計算(int)f
iii.如果該域是long,則計算(int)(f ^ (f >>> 32))
iv.如果該域是float,則計算Float.floatToIntBits(f)
v.如果該域是double,則計算Double.doubleToLongBits(f),然后
? vi.如果該域是一個對象引用,并且該類的equals方法通過遞歸地調用equals的方式來比較這個域,則同樣為這個域遞歸地調用hashCode。如果需要更復雜的比較,則為這個域計算一個“范式”,然后針對這個“范式”調用hashCode。如果域的值為null,則返回0(或其他某個常數,但通常為0)。
vii.如果該域是一個數組,則要吧每一個元素當做單獨的域來處理,也就是要遞歸地應用上述規則,對每個重要的元素計算一個散列碼,然后根據2.b把這些散列值組合起來。如果數組域中的每個元素都很重要,可以使用1.5中增加的其中一個Array.hashCode方法。
b.按照下面的公式,把步驟2.a中計算得到的散列碼c合并到result中:
result = 31 * result + c。(選擇31是因為它是一個奇素數,如果乘數是偶數,乘法溢出時會丟失信息,VM可以優化 31 * i == (i << 5) - i)
3.返回result。
?
編寫完hashCode方法后,編寫單元測試來驗證相同的實例是否有相等的散列碼。
?
把上面的解決方法應用到PhoneNumber類中:
@Override public int hashCode() {int result = 17;result = 31 * result + areaCode;result = 31 * result + prefix;result = 31 * result + lineNumber;return result; }現在使用之前的測試代碼,發現能夠返回Jenny了。
?
如果一個類是不可變的,并且計算散列碼的開銷很大,應該考慮把散列碼緩存到對象內部而不是每次請求都重新計算散列碼,如果這種類大多數對象會被用作散列鍵,應該在創建實例的時候計算散列碼,否則可以選擇延遲初始化散列碼。
?
注意:不要試圖從散列碼計算中排除掉一個對象的關鍵部分來提高性能。雖然這樣做運行起來可能更快,但效果不見得好,在擁有大量實例的時候,忽略的域區別仍然非常大,但散列函數仍然把它們映射到同樣的散列桶中,例如Java 1.2之前實現的String散列函數至多檢查16個字符,對于像URL這樣的大型集合,散列函數表現出病態的行為(把第16個字符后相差非常大的URL映射到同樣的散列桶中,使得碰撞率很高,性能降低)。
轉載于:https://www.cnblogs.com/13jhzeng/p/5644368.html
總結
以上是生活随笔為你收集整理的第9条:覆盖equals时总要覆盖hashCode的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 开源不是免费的
- 下一篇: word文档老是出现这个提示-----“