Effective.Java 讀書筆記(9)關於HashCode

9.Always override hashCode when you override equals

大意爲 在你重寫equals方法的時候要經常重寫hashCode

有很多程序會錯誤的原因之一,就是當你重寫一個類的equals方法的時候忘記重寫它的hashCode了

請記住當你重寫一個類的equals方法的時候,一定要重寫hashCode,如果你不這樣做的話,就會違反了Object.hashCode的
通用規範,這通常會導致一些hash類的問題,比如HashMap,HashSet以及HashTable

以下有一些來自於Object類的規範 [JavaSE6]:
- 當你不斷在一個程序裏面調用一個對象的hashCode方法,它總應該返回一個相同的整形數值

需要注意的是,這個和重寫equals方法的規範中的一致性不大一樣,不要求在反覆執行相同的程序的情況下,返回一樣的值 

 
- 如果兩個對象使用equals方法比較然後返回true的話,那麼這兩個對象的hashCode應該返回相同的數值
- 對於兩個對象使用equals方法比較返回false的情況,並不強制要求hashCode也不一樣

當然,對兩個不同的對象返回不同的hashCode值會提高hashTable的表現

在這裏指出最爲關鍵的部分,即第二個條件是最容易犯錯的,兩個對象用equals比較,一定要返回一樣的hashCode
兩個不一樣的實例可能由於修改了equals方法,可能邏輯上是相等的,但是Object的hashCode並不會去在意這些,只會簡單地返回不一樣的數值,並不會根據規範而返回相同值

舉一個簡單的例子來說,我們來看一個PhoneNumber的類,我們已經重寫了它的構造方法:

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;
    }
    // Broken - no hashCode method!
    ... // Remainder omitted
}

好的,我們用一個HashMap來儲存這個類的實例

Map<PhoneNumber, String> m = new HashMap<PhoneNumber, String>();
m.put(new PhoneNumber(707, 867, 5309), "Jenny")

這個時候,你可能會認爲以下這條語句會返回Jenny

m.get(new PhoneNumber(707 , 867 , 5309))

事實卻是它返回了一個null,明顯存在的問題就是沒有重寫hashCode方法導致這樣,修復這問題只需要重寫一個合適的hashCode方法即可

在這裏要說一點,即使兩個實例運氣足夠好,散列到同一個bucket裏面,HashMap的get方法也會返回null,只要hashCode不一樣HashMap就不會去檢查邏輯上的相等

下面給出一個無腦的解決的方法:

    // The worst possible legal hash function - never use!
    @Override public int hashCode() { return 42; }

這樣看上去好像合法,但是你千萬別這樣用,它合法是由於它保證了相等的對象一定會有相同的hash code,但是這樣的做法太過於粗暴以至於讓所有的對象的hash code都一樣,所有的對象都散列到相同的bucket裏面,HashTable就退化成鏈表了

一個好的hashCode方法對於不同的對象應該返回不同的值,理想情況下,hashCode方法應該均勻地分配數值給那些不相等的對象,但是實現起來好像比較困難,不過接近這樣的方案還是有的:

  1. 存一些連續的非零整形數值,比如說17,把它記爲result
  2. 對於每一個重要的域,做下列的事情:

    1. 對這個域計算int類型的hash code,我們把這個hash code叫做c

      1. 如果這個域是布爾變量,返回(f?1:0)
      2. 如果是byte,char,short或者是int,返回(int)f
      3. 如果是long類型,返回 (int) (f ^ (f >>> 32))
      4. 如果是float類型,返回 Float.floatToIntBits(f)
      5. 如果是double類型,先使用 Double.doubleToLongBits(f) 轉變爲long類型,再按long類型來計算
      6. 如果這個域是對象的引用,而且這個類在使用equals方法的時候會遞歸地來調用這個引用,那就直接遞歸地調用這個引用的hashCode方法,當然如果是需要更加複雜的比較,可以先計算出一個規範的表示,然後在這個規範的表示中去調用hashCode方法,如果該域是null,就直接返回0

        返回0是比較傳統的做法,你也可以返回其他的

      7. 如果域是一個數組,你可以把元素看成是分離的域的組合,對於那些重要的域使用上述的原則進行計算,當然如果整個數組的元素都是重要的,必須要比較的,那你可以直接使用Arrays.hashCode方法

    2. 通過計算得到的result和c我們可以來更新result,公式爲:
      result=result31+c
  3. 返回result
  4. 結束對hashCode方法的編寫,並且檢查有沒有符合上文所說的那幾條規範

在對hash code進行計算的時候,你可能不會把一些“冗餘的”域也計算進去,需要注意的是,那些可以由其他域計算而來的域稱爲冗餘的域,計算hash code的時候把它們忽略不理可能不是一件正確的事,很有可能就會導致對於第二個規範的違反

回看一下計算的過程,我們初始化了一個非零的值,這個值對於hash code的最後生成有着極大的影響,但是這個值不能是0,如果是0的話,那麼初始化的域的影響就沒了,這樣就可能產生衝突,故17這個值是合適的

多維的對於不同類型的不同操作表現出了不錯的hash特性,另外選擇31作爲因子是由於它是一個奇素數,而且利用位運算很容易計算,只要右移5位減去1即可

目前使用素數還是不大明確其優點,但傳統上是這麼用的,在溢出的情況下能夠在一定意義上保留信息

我們使用PhoneNumber類來實際操作一次

@Override public int hashCode() {
    int result = 17;
    result = 31 * result + areaCode;
    result = 31 * result + prefix;
    result = 31 * result + lineNumber;
    return result;
}

這樣的方法實現保證了邏輯相同的實例有着相同的hashCode,這個方法看似簡單,它的性能卻和Java平臺庫的函數性能上不相上下,十分簡單而且高效,將邏輯不同的實例散列到不同的bucket中

需要說明的是,如果計算hash code的代價開銷不小,你必須考慮把hash code緩存起來而不是每一次都重新計算

我們在PhoneNumber類上簡單實現一下

// Lazily initialized, cached hashCode
private volatile int hashCode; 
@Override public int hashCode() {
    int result = hashCode;
    if (result == 0) {
        result = 17;
        result = 31 * result + areaCode;
        result = 31 * result + prefix;
        result = 31 * result + lineNumber;
        hashCode = result;
    }
    return result;
}

這裏所提及的hash函數並不是最先進的,你可以利用數學和計算機科學理論的知識結合最前沿的論文探討一下這個函數的更好的實現方案,但是將一個對象重要的域在hash code的計算中忽略以試圖提高性能的做法絕對是完全錯誤的,最多加快了方法的速度,但對於整個hash集合的性能來說是得不償失的

目前Integer類的hashCode方法都是返回實確的值,這並不是一個好的辦法,希望有一天可以被修改成更爲高效的方法

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