Java hashcode方法編寫技巧 —— 記住這3條約定

本文結合《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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章