所有類都繼承自Object類,他所有的非final方法:equals,hashCode, toString, clone 和 finalize,它們都有通用約定。 我們在覆蓋這些方法的時候需要遵循這些約定,否則依賴這些約定的類(例如HashMap和HashSet)就無法結合該類一起工作了。
一. equals
相等的概念:
-
邏輯相等:例如Integer中包含的數值相等,我們就認為這兩個Integer相等。 再比如AbstractList中如果兩個list包含的所有元素相等則兩個List相等。
-
真正意義上的相等:指向同一個物件。
如果不過載equals函式,那麼兩個類的相等只能是真正意義上的equal。如果類想要自己的相等邏輯就需要像Integer/List那樣過載equals函式。
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的不對稱性
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(); }
備註:
- 在標準的java庫中包含150多個equals方法的實現,包括instanceof檢測, 呼叫getClass檢測, 捕獲ClassCastException檢測或者什麼都不做. 在java.sql.TimeStamp實現人員指出, Timestamp類繼承Date類,而後者的equals方法使用了一個instanceof檢測,這樣重寫equals方法時,就無法同時做到對稱性.
- 在由超類決定相等時,可以考慮final關鍵字修改比較函式,若考慮到子類equals方法靈活性,可以不加修飾,例如AbstractSet.equals方法,應該申明為final, 這樣就可以比較子類HashSet和TreeSet, 但是考慮到子類的靈活性,沒有新增任何修飾.
編寫equals方法的建議:
- 顯示引數命名為otherObject, 稍後轉化成other變數
public boolean equals(Object otherObject)
- 檢測this和otherObject是否是同一個物件的引用,是,返回true;
if(this==otherObject){
return true;
} - 檢測otherObject是否為null, 是, 返回false;
if(otherObject == null){
return false;
} - 比較this和otherObject是否屬於同一個類. 如果equals的語義在每個子類中有所改變,就使用getClass檢測:
if(getClass() != otherObject.getClass()){
return false;
}如果所以子類語義相同,使用instanceof檢測:
if(!(otherObject instanceof Employee)){
return false;
} - 將otherObject轉化為相對應的型別變數other
Employee other = (Employee)otherObject;
- 對所需要的比較的資料域進行比較. 如果是基本資料型別,使用a==b比較; 如果是物件比較,呼叫Objects.equals(a, b)進行比較
return Objects.equals(name, other.name) && salary == other.salary && Objects.equals(hireDay, other.hireDay);
二、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了。