深度解讀equal方法與hashCode方法淵源
深度解讀equal方法與hashCode方法淵源
大部分內容參考自重寫equal()時為什麼也得重寫hashCode()之深度解讀equal方法與hashCode方法淵源
1. equals()的所屬以及內部原理(即Object中equals方法的實現原理)
說起equals方法,我們都知道是超類Object中的一個基本方法,用於檢測一個物件是否與另外一個物件相等。而在Object類中這個方法實際上是判斷兩個物件是否具有相同的引用,如果有,它們就一定相等。其原始碼如下:
public boolean equals(Object obj){
return (this == obj);
}
實際上我們知道所有的物件都擁有標識(記憶體地址)和狀態(資料),同時“==”比較兩個物件的的記憶體地址,所以說 Object 的 equals() 方法是比較兩個物件的記憶體地址是否相等,即若 object1.equals(object2) 為 true,則表示 equals1 和 equals2 實際上是引用同一個物件。
2. equals()和’=='的區別
常常我們會說:
- equals比較的是物件的內容
- ==比較的是物件的地址
但是從前面我們可以知道equals方法在Object中的實現也是間接使用了‘==’運算子進行比較的,所以從嚴格意義上來說,不正確。
public class Car{
private int batch;
public Car(int batch){
this.batch = batch;
}
public static void main(String[] args){
Car c1 = new Car(1);
Car c2 = new Car(1);
System.out.println(c1.equals(c2));
System.out.println(c1 == c2);
}
}
結果:
false
false
分析
- 對於’=='返回false
- 比較的是記憶體地址,而c1與c2是兩個不同的物件,所以c1與c2的記憶體地址自然也不一樣
- 對於equals返回false
- 並沒有重寫equals()方法,呼叫的是Object超類的原始equals方法,其內部實現使用的是’=='運算子
但我們想讓equals返回true,該如何實現呢?
為了達到我們的期望值,我們必須重寫Car的equal方法,讓其比較的是物件的批次(即物件的內容),而不是比較記憶體地址,於是修改如下:
@Override
public boolean equals(Object obj) {
if (obj instanceof Car) {
Car c = (Car) obj;
return batch == c.batch;
}
return false;
}
使用instanceof來判斷引用obj所指向的物件的型別,如果obj是Car類物件,就可以將其強制轉為Car物件
結果:
true
false
總結
預設情況下也就是從超類Object繼承而來的equals方法與‘==’是完全等價的,比較的都是物件的記憶體地址,但我們可以重寫equals方法,使其按照我們的需求的方式進行比較,如String類重寫了equals方法,使其比較的是字元的序列,而不再是記憶體地址。
3. equals()的重寫規則
在重寫equals方法時,還是需要注意如下幾點規則:
-
自反性:對於任何非null的引用值x,x.equals(x)應返回true
-
對稱性:對於任何非null的引用值x與y,當且僅當:y.equals(x)返回true時,x.equals(y)才返回true
-
傳遞性:對於任何非null的引用值x、y與z,如果y.equals(x)返回true,y.equals(z)返回true,那麼x.equals(z)也應返回true
-
一致性:對於任何非null的引用值x與y,假設物件上equals比較中的資訊沒有被修改,則多次呼叫x.equals(y)始終返回true或者始終返回false
-
對於任何非空引用值x,x.equal(null)應返回false
在同一個類的兩個物件間的比較這裡就略過了,還是相當容易理解的。但是如果是子類與父類混合比較,那麼情況就不太簡單了。下面我們來看看另一個例子,首先,我們先建立一個新類BigCar,繼承於Car,然後進行子類與父類間的比較。
public class BigCar extends Car {
int count;
public BigCar(int batch, int count) {
super(batch);
this.count = count;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof BigCar) {
BigCar bc = (BigCar) obj;
return super.equals(bc) && count == bc.count;
}
return false;
}
public static void main(String[] args) {
Car c = new Car(1);
BigCar bc = new BigCar(1, 20);
System.out.println(c.equals(bc));
System.out.println(bc.equals(c));
}
}
執行結果:
true
false
分析
- BigCar型別肯定是屬於Car型別,所以c.equals(bc)肯定為true
- Car型別並不一定是BigCar型別(Car類還可以有其他子類),所以bc.equals©返回false
但如果有這樣一個需求,只要BigCar和Car的生產批次一樣,我們就認為它們兩個是相當的,在這樣一種需求的情況下,父類(Car)與子類(BigCar)的混合比較就不符合equals方法對稱性特性了。
很明顯一個返回true,一個返回了false,根據對稱性的特性,此時兩次比較都應該返回true才對。那麼該如何修改才能符合對稱性呢?
其實造成不符合對稱性特性的原因很明顯,那就是因為Car型別並不一定是BigCar型別(Car類還可以有其他子類),在這樣的情況下(Car instanceof BigCar)永遠返回false,因此,我們不應該直接返回false,而應該繼續使用父類的equals方法進行比較才行(因為我們的需求是批次相同,兩個物件就相等,父類equals方法比較的就是batch是否相同)。
@Override
public boolean equals(Object obj) {
if (obj instanceof BigCar) {
BigCar bc = (BigCar) obj;
return super.equals(bc) && count == bc.count;
}
return super.equals(obj);
}
此時符合了對稱性,但還沒有符合傳遞性
public class BigCar extends Car {
int count;
public BigCar(int batch, int count) {
super(batch);
this.count = count;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof BigCar) {
BigCar bc = (BigCar) obj;
return super.equals(bc) && count == bc.count;
}
return super.equals(obj);
}
public static void main(String[] args) {
Car c = new Car(1);
BigCar bc = new BigCar(1, 20);
BigCar bc2 = new BigCar(1, 22);
System.out.println(bc.equals(c));
System.out.println(c.equals(bc2));
System.out.println(bc.equals(bc2));
}
}
結果:
true
true
false
bc,bc2,c的批次都是相同的,按我們之前的需求應該是相等,而且也應該符合equals的傳遞性才對。但是事實上執行結果卻不是這樣,違背了傳遞性。
出現這種情況根本原因在於:
- 父類與子類進行混合比較。
- 子類中聲明瞭新變數,並且在子類equals方法使用了新增的成員變數作為判斷物件是否相等的條件。
只要滿足上面兩個條件,equals方法的傳遞性便失效了。而且目前並沒有直接的方法可以解決這個問題。因此我們在重寫equals方法時這一點需要特別注意。雖然沒有直接的解決方法,但是間接的解決方案還說有滴,那就是通過組合的方式來代替繼承,還有一點要注意的是組合的方式並非真正意義上的解決問題(只是讓它們間的比較都返回了false,從而不違背傳遞性,然而並沒有實現我們上面batch相同物件就相等的需求),而是讓equals方法滿足各種特性的前提下,讓程式碼看起來更加合情合理,程式碼如下:
public class Combination4BigCar {
private Car c;
private int count;
public Combination4BigCar(int batch, int count) {
c = new Car(batch);
this.count = count;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Combination4BigCar) {
Combination4BigCar bc = (Combination4BigCar) obj;
return c.equals(bc.c) && count == bc.count;
}
return false;
}
}
從程式碼來看即使batch相同,Combination4BigCar類的物件與Car類的物件間的比較也永遠都是false,但是這樣看起來也就合情合理了,畢竟Combination4BigCar也不是Car的子類,因此equals方法也就沒必要提供任何對Car的比較支援,同時也不會違背了equals方法的傳遞性。
4. 為什麼重寫equals()的同時還得重寫hashCode()
這個問題主要針對Map介面。當我們呼叫put或者get方法對Map容器操作時,是根據鍵物件的雜湊碼來計算儲存位置的。在java中,我們可以使用hasCode()來獲取物件的雜湊碼,其值就是物件的儲存地址。
hashCode的意思是雜湊碼,也就是雜湊碼,是由物件到處的一個整型值,雜湊碼是沒有規律的,如果x與y是兩個不同的物件,那麼x.hashCode()與y.hashCode()基本是不會相同的,下面通過String類的hashCode()計算一組雜湊碼:
public class HashCodeTest {
public static void main(String[] args) {
int hash=0;
String s="ok";
StringBuilder sb =new StringBuilder(s);
System.out.println(s.hashCode()+" "+sb.hashCode());
String t = new String("ok");
StringBuilder tb =new StringBuilder(s);
System.out.println(t.hashCode()+" "+tb.hashCode());
}
}
結果:
3548 1829164700
3548 2018699554
分析
- 字串s與t擁有相同的雜湊碼,這是因為字串的雜湊碼是由內容匯出的
- 符串緩衝sb與tb卻有著不同的雜湊碼,這是因為StringBuilder沒有重寫hashCode方法,它的雜湊碼是由Object類預設的hashCode方法計算出來的物件儲存地址
重寫equals方法時也必須重寫hashCode方法
在Java API文件中關於hashCode方法有以下幾點規定:
- 在java應用程式執行期間,如果在equals方法比較中所用的資訊沒有被修改,那麼在同一個物件上多次呼叫hashCode方法時必須一致地返回相同的整數。如果多次執行同一個應用時,不要求該整數必須相同。
- 如果兩個物件通過呼叫equals方法是相等的,那麼這兩個物件呼叫hashCode方法必須返回相同的整數。
- 如果兩個物件通過呼叫equals方法是不相等的,不要求這兩個物件呼叫hashCode方法必須返回不同的整數。但是程式設計師應該意識到對不同的物件產生不同的hash值可以提供雜湊表的效能。
如果一個類重寫了equals方法,但沒有重寫hashCode方法,將會直接違法了第2條規定,這樣的話,如果我們通過對映表(Map介面)操作相關物件時,就無法達到我們預期想要的效果。
5. 重寫equals()中getClass與instanceof的區別
在重寫equals() 方法時,一般都是推薦使用 getClass 來進行型別判斷(除非所有的子類有統一的語義才使用instanceof),不是使用 instanceof。
- instanceof 的作用:判斷其左邊物件是否為其右邊類的例項,返回 boolean 型別的資料
父類Person:
public class Person {
protected String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Person(String name){
this.name = name;
}
public boolean equals(Object object){
if(object instanceof Person){
Person p = (Person) object;
if(p.getName() == null || name == null){
return false;
}
else{
return name.equalsIgnoreCase(p.getName ());
}
}
return false;
}
}
子類 Employee:
public class Employee extends Person{
private int id;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public Employee(String name,int id){
super(name);
this.id = id;
}
/**
* 重寫equals()方法
*/
public boolean equals(Object object){
if(object instanceof Employee){
Employee e = (Employee) object;
return super.equals(object) && e.getId() == id;
}
return false;
}
}
上面父類 Person 和子類 Employee 都重寫了 equals(),不過 Employee 比父類多了一個id屬性,而且這裡我們並沒有統一語義。測試程式碼如下:
public class Test {
public static void main(String[] args) {
Employee e1 = new Employee("chenssy", 23);
Employee e2 = new Employee("chenssy", 24);
Person p1 = new Person("chenssy");
System.out.println(p1.equals(e1));
System.out.println(p1.equals(e2));
System.out.println(e1.equals(e2));
}
}
上面程式碼我們定義了兩個員工和一個普通人,雖然他們同名,但是他們肯定不是同一人,所以按理來說結果應該全部是 false,但是事與願違,結果是:true、true、false。對於那 e1!=e2 我們非常容易理解,因為他們不僅需要比較 name,還需要比較 ID。但是 p1 即等於 e1 也等於 e2,這是非常奇怪的,因為 e1、e2 明明是兩個不同的類,但為什麼會出現這個情況?首先 p1.equals(e1),是呼叫 p1 的 equals 方法,該方法使用 instanceof 關鍵字來檢查 e1 是否為 Person 類,這裡我們再看看 instanceof:判斷其左邊物件是否為其右邊類的例項,也可以用來判斷繼承中的子類的例項是否為父類的實現。他們兩者存在繼承關係,肯定會返回 true 了,而兩者 name 又相同,所以結果肯定是 true。所以出現上面的情況就是使用了關鍵字 instanceof,這是非常容易導致我們“鑽牛角尖”。故在覆寫 equals 時推薦使用 getClass 進行型別判斷。而不是使用 instanceof(除非子類擁有統一的語義)。
6. 編寫一個完美equals()的幾點建議
出自Java核心技術 第一卷:基礎知識:
- 顯式引數命名為otherObject,稍後需要將它轉換成另一個叫做other的變數(引數名命名,強制轉換請參考建議5)
- 檢測this與otherObject是否引用同一個物件 :if(this == otherObject) return true;(儲存地址相同,肯定是同個物件,直接返回true)
- 檢測otherObject是否為null ,如果為null,返回false.if(otherObject == null) return false;
- 比較this與otherObject是否屬於同一個類 (視需求而選擇)
- 如果equals的語義在每個子類中有所改變,就使用getClass檢測 :if(getClass()!=otherObject.getClass()) return false; (參考前面分析的第5點)
- 如果所有的子類都擁有統一的語義,就使用instanceof檢測 :if(!(otherObject instanceof ClassName)) return false;(即前面我們所分析的父類car與子類bigCar混合比,我們統一了批次相同即相等)
- 將otherObject轉換為相應的類型別變數:ClassName other = (ClassName) otherObject;
- 現在開始對所有需要比較的域進行比較 。使用==比較基本型別域,使用equals比較物件域。如果所有的域都匹配,就返回true,否則就返回flase。
- 如果在子類中重新定義equals,就要在其中包含呼叫super.equals(other)
- 當此方法被重寫時,通常有必要重寫 hashCode 方法,以維護 hashCode 方法的常規協定,該協定宣告 相等物件必須具有相等的雜湊碼
參考資料:
Java核心技術 第一卷:基礎知識
Java深入分析