java自定義equals函數和hashCode函數

 

全部類都繼承自Object類,他全部的非final方法:equals,hashCode, toString, clone 和 finalize,它們都有通用約定。 咱們在覆蓋這些方法的時候須要遵循這些約定,不然依賴這些約定的類(例如HashMap和HashSet)就沒法結合該類一塊兒工做了。java

 

一. equals

相等的概念:

  • 邏輯相等:例如Integer中包含的數值相等,咱們就認爲這兩個Integer相等。 再好比AbstractList中若是兩個list包含的全部元素相等則兩個List相等。
  • 真正意義上的相等:同一個對象。

若是不重載equals函數,那麼兩個類的相等只能是真正意義上的equal。若是類想要本身的相等邏輯就須要像Integer/List那樣重載equals函數。sql

 

java規範中equals方法特徵

  • 自反性 : 對於任何非空引用x, x.equals(x) 返回true;
  • 對稱性: 對於任何引用x, y, 當且僅當y.equals(x) 返回true, x.equals(y)返回true;
  • 傳遞性: 對於任何引用x, y, z, 若x.equals(y)返回true, y.equals(z)返回true; 則 x.equals(z)返回true;
  • 一致性: 若x和y引用的對象沒有發生改變, 則反覆調用x.equals(y)應該返回一樣的結果.
  • 對任意非空引用x, x.equals(null) 返回false;

下面能夠經過兩個不一樣的狀況看待這個問題:數組

  • 若是子類可以擁有本身的相等概念, 則對稱性需求強制採用getClass進行檢測
  • 若是由超類決定相等的概念, 那麼就用instanceof進行檢測,這樣能夠在不用子類的對象之間進行相等的比較

 

TimeStamp的不對稱性ide

Date date = new Date();
Timestamp t1 = new Timestamp(date.getTime());

System.out.println("Date equals Timestamp ? : " +  date.equals(t1));// true
System.out.println("Timestamp equals Date ? : " +  t1.equals(date));// false

 

TimeStamp源碼:(使用了instanceof 而不是 getClass())函數

    // Timestamp
    @Override
    public boolean equals(java.lang.Object ts) {
        if (ts instanceof Timestamp) {
            return this.equals((Timestamp)ts);
        } else {
            return false;// 非Timestamp 實例直接返回false
        }
    }
    // 省略其餘代碼
    public boolean equals(Timestamp ts) {
        if (super.equals(ts)) {
            if  (nanos == ts.nanos) {
                return true;
            } else {
                return false;
            }
        } else {
            return false;
        }
    }

父類Date:性能

    // Date
    @Override
    public boolean equals(Object obj) {
        return obj instanceof Date && getTime() == ((Date) obj).getTime();
    }

 

備註:

  1. 在標準的java庫中包含150多個equals方法的實現,包括instanceof檢測, 調用getClass檢測, 捕獲ClassCastException檢測或者什麼都不作. 在java.sql.TimeStamp實現人員指出, Timestamp類繼承Date類,然後者的equals方法使用了一個instanceof檢測,這樣重寫equals方法時,就沒法同時作到對稱性.
  2. 在由超類決定相等時,能夠考慮final關鍵字修改比較函數,若考慮到子類equals方法靈活性,能夠不加修飾,例如AbstractSet.equals方法,應該申明爲final, 這樣就能夠比較子類HashSet和TreeSet, 可是考慮到子類的靈活性,沒有添加任何修飾.

 

編寫equals方法的建議:

  1. 顯示參數命名爲otherObject, 稍後轉化成other變量

    public boolean equals(Object otherObject)單元測試

  2. 檢測this和otherObject是不是同一個對象的引用,是,返回true;

    if(this==otherObject){
    return true;
    }測試

  3. 檢測otherObject是否爲null, 是, 返回false;

    if(otherObject == null){
    return false;
    }優化

  4. 比較this和otherObject是否屬於同一個類. 若是equals的語義在每一個子類中有所改變,就使用getClass檢測:

    if(getClass() != otherObject.getClass()){
    return false;
    }this

    若是因此子類語義相同,使用instanceof檢測:

    if(!(otherObject instanceof Employee)){
    return false;
    }

     

  5. 將otherObject轉化爲相對應的類型變量other

    Employee other = (Employee)otherObject;

  6. 對所須要的比較的數據域進行比較. 若是是基本數據類型,使用a==b比較; 若是是對象比較,調用Objects.equals(a, b)進行比較

    return Objects.equals(name, other.name) && salary == other.salary && Objects.equals(hireDay, other.hireDay);

 

2、hashCode()

設計原則中有一條: 覆蓋equals時總要覆蓋hashCode

hashCode編碼原則:

1.只要對象equals方法的比較操做所用到的信息沒有被修改,對同一對象調用屢次,hashCode方法都必須返回同一整數。在同一應用程序的屢次執行過程當中,每次執行返回的整數能夠不一致。

2.若是兩個對象根據equals(Object)方法比較是相等的,那麼這兩個對象的hashCode返回值相同。

3.若是兩個對象根據equals(Object)方法比較是不等的,那麼這兩個對象的hashCode返回值不必定不等,可是給不一樣的對象產生大相徑庭的整數結果,能提升散列表的性能。


具體實例

若是一個類覆蓋了equals覆蓋了equals函數,卻沒有覆蓋hashCode會違反上述第二條原則。下面看一下沒有重載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);

        }
    }

    @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;
    }

}

執行以下代碼:

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了。

相關文章
相關標籤/搜索