本文結合《Effective Java》第三章條目9《覆蓋equals時總要覆蓋hashCode》和自己的理解及實踐,講解了在覆蓋hashCode時需要遵守的規範,文章發佈於專欄Effective Java,歡迎讀者訂閱。
Java的hashCode方法,int hashCode(),沒有入參,返回一個int,是每個對象都有的方法,這個方法有什麼用?編寫時需要注意什麼?
hashCode方法有什麼用
hashCode方法,主要應用於散列集合的桶存放和查找算法中,這樣的集合包括HashMap、HashSet、HashTable等。
這些集合,在存放元素的時候,會根據元素的hashcode方法的返回值,決定元素要放在哪個桶裏面,這樣做的目的是提高查找的效率,在查找的時候,就可以根據對象的hashcode返回值,直接定位到對象在哪個桶裏面,然後再到桶裏面,去調用equals方法查找這個對象。關於equals方法的介紹,可以閱讀專欄的另一篇文章
Java equals方法編寫規範 —— 牢記這五條軍規
編寫hashCode方法時要遵守的三條約定
1、一物一桶:如果x.equals(y)==true,那麼x.hashCode() == y.hashCode()。
2、不能換桶:在應用程序執行期間,只要對象的equals方法所用到的信息沒有改變,那麼對這個對象調用多次hashCode方法,會一直返回同一個整數。
3、一桶一物:如果x.equals(y)==false,那麼x和y的hashCode方法,儘量要產生不一樣的結果,但原則上可以產生一樣的結果。也就是說,一個桶裏面,可以放多個對象,但是,按照上一節所講的,一個桶裏的對象越多,在查找的時候就要花費更多的時間,散列表的性能會下降,如果一個桶裏面放的對象過多,那麼也就起不到hash集合的優勢了。
爲什麼覆蓋了equals方法後一定要覆蓋hashcode方法
原因很簡單,因爲如果不覆蓋,那麼由於Object的hashCode方法會返回隨意的一個整數,因此兩個equals的對象,hashCode方法返回值不同,違反了上一節的第一條約定。
那麼,重點來了,爲什麼我們要遵守第一條約定呢?
假設有一個PhoneNumber類,通過equals實現了自己的"邏輯相等":
public final class PhoneNumber {
private final short areaCode;
private final short prefix;
private final short 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 = (short) areaCode;
this.prefix = (short) prefix;
this.lineNumber = (short) lineNumber;
}
private static void rangeCheck(int arg, int max,
String name) {
if (arg < 0 || arg > max)
throw new IllegalArgumentException(name +": " + arg);
}
@Override public 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;
}
}
然後現在有這麼一個調用方法:
public static void main(String[] args) {
Map<PhoneNumber, String> m
= new HashMap<PhoneNumber, String>();
m.put(new PhoneNumber(707, 867, 5309), "Jenny");
System.out.println(m.get(new PhoneNumber(707, 867, 5309)));
}
我們可能期望他會返回"Jenny”,但是結果卻是返回null。原因在於,前後兩個new出來的對象,hashCode返回值不一樣,因此,在put的時候,hashMap會把對象放到桶1,然後在get時,hashMap卻去桶2尋找這個對象,自然就找不到這個對象了。
要修正這個問題,我們只需要編寫一個hashCode方法即可,怎麼編寫一個好的hashCode方法呢?
一個例子 學習怎樣編寫高質量的hashcode方法
針對上一小節講的例子,我們給它編寫了一個hashCode方法:
@Override
public int hashCode() {
int result = 17;
result = 31 * result + areaCode;
result = 31 * result + prefix;
result = 31 * result + lineNumber;
return result;
}
這個方法,爲了滿足約定1和2,使用了equals方法用到的三個屬性,利用這三個屬性,去計算hashCode。看了這個方法,或許會有如下疑問,來,一個一個解答:
1、爲什麼要用乘法,直接把三個屬性相加,然後返回,不行嗎?
答:行是可行,但是很容易導致不equals的兩個對象,hashCode相等,也就是說,很容易違反約定3,。爲什麼呢?假設這樣寫:
@Override
public int hashCode() {
int result = areaCode + prefix + lineNumber;
return result;
}
那麼對於new PhoneNumber(707, 867, 5309) 和 new PhoneNumber(706, 868, 5309),這兩個不相同的對象,hashCode是不是就相等了?所以,乘法的目的,是爲了讓散列值依賴於屬性的順序,降低不同對象產生相同的hashCode的概率。
2、爲什麼要用31去乘?
《Effective Java》給的解釋是:
a. 31有一個很好的特性,即用移位和減法代替乘法,31*i == (i<<5)-i,JVM會自動做這個優化
b. 31是奇素數,如果使用偶數,並且乘法溢出,信息就會丟失(這一點不是很懂)
3、如果屬性不是int,而是long或者其他的怎麼辦?
原則很簡單,就是把不是int的屬性,轉爲int屬性,並且保證不容易重複。
比如:
long類型的屬性f,可以這樣(int)(f^(f>>>32))
float類型:Float.floatToIntBits(f)
String類型:直接調用它的hashCode方法
依此類推...
總結
hashCode方法主要用在散列集合的元素存放和查找算法中。
hashCode方法要遵守三條約定:一物一桶,不能換桶,一桶一物。
覆蓋equals方法之後一定要覆蓋hashCode方法。
編寫hashCode方法時,牢記一個數字——31,一個運算法——乘法,一個原則——轉爲int。