更好的重寫hashCode方法

重寫hashCode的規範

每一個重寫equals方法的類中,也必須重寫hashCode方法。java

若是不覆蓋hashCode,會致使沒法結合基於散列的集合正常工做,例如HashMap、HashSet和Hashtable等等,換句話說,實現了對的hashCode,就能夠拿對象的實例做爲Hash集合的Key,下面是重寫hashCode的規範:程序員

  • 在應用程序執行期間,只要對象的equals方法的比較操做所用到的信息沒有被修改,那麼對這同一個對象調用屢次,hashCode方法都必須始終如一地返回同一個整數。在同一個應用程序的屢次執行過程當中,每次執行所返回的整數能夠不一致;
  • 若是兩個對象根據equals方法比較是相等的,那麼調用這兩個對象中任何一個對象的hashCode方法都必須產生一樣的整數結果;
  • 若是兩個對象根據equals方法比較是不相等的,那麼調用這兩個對象中任意一個對象的hashCode方法,則不必定要產生不一樣的結果。可是程序員應該知道,給不相等的對象產生大相徑庭的整數結果,有可能提升山列表(hash table)的性能;

相等的對象必須具備相等的散列碼(hash code)。數組

兩個不一樣的實例在邏輯上有多是相等的(equals),可是hashCode方法返回的應該是兩個不一樣的隨機整數,考慮下面這個PhoneNumber類,在企圖與HashMap一塊兒使用時,將失敗:緩存

package test.ch02;

public class PhoneNumber {

    private final int areaCode;
    private final int prefix;
    private final int lineNumber;

    public PhoneNumber(int areaCode, int prefix, int lineNumber) {
        this.areaCode = areaCode;
        this.prefix = prefix;
        this.lineNumber = lineNumber;
    }

    public int getAreaCode() {
        return areaCode;
    }

    public int getPrefix() {
        return prefix;
    }

    public int getLineNumber() {
        return lineNumber;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof PhoneNumber)) {
            return false;
        }
        PhoneNumber pn = (PhoneNumber) o;
        return pn.lineNumber == lineNumber && pn.prefix == prefix && pn.areaCode == areaCode;
    }

}
package test.ch02;

import java.util.HashMap;
import java.util.Map;

public class Test {

    public static void main(String[] args) {
        Map<PhoneNumber, String> m = new HashMap<>();
        m.put(new PhoneNumber(707, 867, 5309), "Jenny");
        String s = m.get(new PhoneNumber(707, 867, 5309));
        System.out.println(s); // null
    }

}

因爲PhoneNumber沒有重寫hashCode方法,從而致使兩個相等的實例具備不相等的散列碼。ide

修正這個問題很簡單,只須要爲PhoneNumber提供一個適當的hashCode便可。函數

如何重寫規範的hashCode方法

一個好的hashCode方法傾向於「爲不相等的對象產生不相等的散列碼」。性能

理想狀況下,散列函數應該把集合中不相等的實例均勻地分佈到全部可能的散列值上,能夠採用以下作法:優化

  1. 把某個非零的常數值,好比17,保存在一個result的int類型變量中;
  2. 對於對象中每一個關鍵域f,完成如下步驟:
  • 爲該域計算int類型的散列碼c:
  • 若是該域是boolean,則計算(f ? 1 : 0);
  • 若是該域是byte、char、short或者int,則計算(int)f;
  • 若是該域是long,則計算(int)(f^(f>>>32));
  • 若是該域是float,則計算Float.floatToIntBits(f);
  • 若是該域是double,則計算Double.doubleToLongBits(f),在根據long計算;
  • 若是該域是一個對象引用,而且該類的equals方法經過遞歸地調用equals的方式來比較這個域,則一樣爲這個域遞歸地調用hashCode;
  • 若是該域是一個數組,則要把每個元素看成單獨的域來處理,也可使用Arrays.hashCode方法;
  • 按照result = 31 * result + c來計算散列碼;

爲PhoneNumber重寫一個hashCode:this

package test.ch02;

public class PhoneNumber {

    private final int areaCode;
    private final int prefix;
    private final int lineNumber;

    public PhoneNumber(int areaCode, int prefix, int lineNumber) {
        this.areaCode = areaCode;
        this.prefix = prefix;
        this.lineNumber = lineNumber;
    }

    public int getAreaCode() {
        return areaCode;
    }

    public int getPrefix() {
        return prefix;
    }

    public int getLineNumber() {
        return lineNumber;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof PhoneNumber)) {
            return false;
        }
        PhoneNumber pn = (PhoneNumber) o;
        return pn.lineNumber == lineNumber && pn.prefix == prefix && pn.areaCode == areaCode;
    }

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

}
package test.ch02;

import java.util.HashMap;
import java.util.Map;

public class Test {

    public static void main(String[] args) {
        Map<PhoneNumber, String> m = new HashMap<>();
        m.put(new PhoneNumber(707, 867, 5309), "Jenny");
        String s = m.get(new PhoneNumber(707, 867, 5309));
        System.out.println(s); // Jenny
    }

}

優化hashCode方法

  • 在散列碼的計算過程當中,能夠把冗餘域(redundant field)排除在外。
  • 爲何選擇31作散列碼,是由於它是一個奇素數。31有個很好的特性,即用位移法和減法來代替乘法,能夠獲得更好的性能: 31 * i = (i<<5)-i。現代的VM能夠自動完成這種優化;
  • 若是一個類是不可變的,而且計算散列碼的開銷很大,就應該考慮把散列碼緩存在對象內部。或者考慮延遲初始化散列碼,在第一次調用hashCode時緩存到內部;
  • 不要視圖從散列碼計算中排除掉一個對象的關鍵部分來提升性能;
相關文章
相關標籤/搜索