今天這篇文章我們打算來深度解讀一下equal方法以及其關聯方法hashCode(),我們準備從以下幾點入手分析:
1.equals()的所屬以及內部原理(即Object中equals方法的實現原理)
說起equals方法,我們都知道是超類Object中的一個基本方法,用於檢測一個物件是否與另外一個物件相等。而在Object類中這個方法實際上是判斷兩個物件是否具有相同的引用,如果有,它們就一定相等。其原始碼如下:
1 |
public boolean equals(Object obj) { return (this == obj); } |
實際上我們知道所有的物件都擁有標識(記憶體地址)和狀態(資料),同時“==”比較兩個物件的的記憶體地址,所以說 Object 的 equals() 方法是比較兩個物件的記憶體地址是否相等,即若 object1.equals(object2) 為 true,則表示 equals1 和 equals2 實際上是引用同一個物件。
2.equals()與‘==’的區別
或許這是我們面試時更容易碰到的問題”equals方法與‘==’運算子有什麼區別?“,並且常常我們都會胸有成竹地回答:“equals比較的是物件的內容,而‘==’比較的是物件的地址。”。但是從前面我們可以知道equals方法在Object中的實現也是間接使用了‘==’運算子進行比較的,所以從嚴格意義上來說,我們前面的回答並不完全正確。我們先來看一段程式碼並執行再來討論這個問題。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
package com.zejian.test; 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 |
分析:對於‘==’運算子比較兩個Car物件,返回了false,這點我們很容易明白,畢竟它們比較的是記憶體地址,而c1與c2是兩個不同的物件,所以c1與c2的記憶體地址自然也不一樣。現在的問題是,我們希望生產的兩輛的批次(batch)相同的情況下就認為這兩輛車相等,但是執行的結果是儘管c1與c2的批次相同,但equals的結果卻反回了false。當然對於equals返回了false,我們也是心知肚明的,因為equal來自Object超類,訪問修飾符為public,而我們並沒有重寫equal方法,故呼叫的必然是Object超類的原始方equals方法,根據前面分析我們也知道該原始equal方法內部實現使用的是’==’運算子,所以返回了false。因此為了達到我們的期望值,我們必須重寫Car的equal方法,讓其比較的是物件的批次(即物件的內容),而不是比較記憶體地址,於是修改如下:
1 2 3 4 5 6 7 8 |
@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物件,然後比較兩輛Car的批次,相等返回true,否則返回false。當然如果obj不是 Car物件,自然也得返回false。我們再次執行:
true false |
嗯,達到我們預期的結果了。因為前面的面試題我們應該這樣回答更佳
總結:預設情況下也就是從超類Object繼承而來的equals方法與‘==’是完全等價的,比較的都是物件的記憶體地址,但我們可以重寫equals方法,使其按照我們的需求的方式進行比較,如String類重寫了equals方法,使其比較的是字元的序列,而不再是記憶體地址。
3.equals()的重寫規則
前面我們已經知道如何去重寫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。
當然在通常情況下,如果只是進行同一個類兩個物件的相等比較,一般都可以滿足以上5點要求,下面我們來看前面寫的一個例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
package com.zejian.test; 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); Car c3 = new Car(1); System.out.println("自反性->c1.equals(c1):" + c1.equals(c1)); System.out.println("對稱性:"); System.out.println(c1.equals(c2)); System.out.println(c2.equals(c1)); System.out.println("傳遞性:"); System.out.println(c1.equals(c2)); System.out.println(c2.equals(c3)); System.out.println(c1.equals(c3)); System.out.println("一致性:"); for (int i = 0; i < 50; i++) { if (c1.equals(c2) != c1.equals(c2)) { System.out.println("equals方法沒有遵守一致性!"); break; } } System.out.println("equals方法遵守一致性!"); System.out.println("與null比較:"); System.out.println(c1.equals(null)); } @Override public boolean equals(Object obj) { if (obj instanceof Car) { Car c = (Car) obj; return batch == c.batch; } return false; } } |
執行結果:
自反性->c1.equals(c1):true 對稱性: true true 傳遞性: true true true 一致性: equals方法遵守一致性! 與null比較: false |
由執行結果我們可以看出equals方法在同一個類的兩個物件間的比較還是相當容易理解的。但是如果是子類與父類混合比較,那麼情況就不太簡單了。下面我們來看看另一個例子,首先,我們先建立一個新類BigCar,繼承於Car,然後進行子類與父類間的比較。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
package com.zejian.test; 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,對於bc.equals(c)返回false,是因為Car型別並不一定是BigCar型別(Car類還可以有其他子類)。嗯,確實是這樣。但如果有這樣一個需求,只要BigCar和Car的生產批次一樣,我們就認為它們兩個是相當的,在這樣一種需求的情況下,父類(Car)與子類(BigCar)的混合比較就不符合equals方法對稱性特性了。很明顯一個返回true,一個返回了false,根據對稱性的特性,此時兩次比較都應該返回true才對。那麼該如何修改才能符合對稱性呢?其實造成不符合對稱性特性的原因很明顯,那就是因為Car型別並不一定是BigCar型別(Car類還可以有其他子類),在這樣的情況下(Car instanceof BigCar)永遠返回false,因此,我們不應該直接返回false,而應該繼續使用父類的equals方法進行比較才行(因為我們的需求是批次相同,兩個物件就相等,父類equals方法比較的就是batch是否相同)。因此BigCar的equals方法應該做如下修改:
1 2 3 4 5 6 7 8 |
@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); } |
這樣執行的結果就都為true了。但是到這裡問題並沒有結束,雖然符合了對稱性,卻還沒符合傳遞性,例項如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
package com.zejian.test; 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方法滿足各種特性的前提下,讓程式碼看起來更加合情合理,程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package com.zejian.test; 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()的重寫規則之必要性深入解讀
前面我們一再強調了equals方法重寫必須遵守的規則,接下來我們就是分析一個反面的例子,看看不遵守這些規則到底會造成什麼樣的後果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
package com.zejian.test; import java.util.ArrayList; import java.util.List; /** * 反面例子 * @author zejian */ public class AbnormalResult { public static void main(String[] args) { List<A> list = new ArrayList<A>(); A a = new A(); B b = new B(); list.add(a); System.out.println("list.contains(a)->" + list.contains(a)); System.out.println("list.contains(b)->" + list.contains(b)); list.clear(); list.add(b); System.out.println("list.contains(a)->" + list.contains(a)); System.out.println("list.contains(b)->" + list.contains(b)); } static class A { @Override public boolean equals(Object obj) { return obj instanceof A; } } static class B extends A { @Override public boolean equals(Object obj) { return obj instanceof B; } } } |
上面的程式碼,我們宣告瞭 A,B兩個類,注意必須是static,否則無法被main呼叫。B類繼承A,兩個類都重寫了equals方法,但是根據我們前面的分析,這樣重寫是沒有遵守對稱性原則的,我們先來看看執行結果:
list.contains(a)->true list.contains(b)->false list.contains(a)->true list.contains(b)->true |
19行和24行的輸出沒什麼好說的,將a,b分別加入list中,list中自然會含有a,b。但是為什麼20行和23行結果會不一樣呢?我們先來看看contains方法內部實現
1 2 3 4 |
@Override public boolean contains(Object o) { return indexOf(o) != -1; } |
進入indexof方法
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@Override ublic int indexOf(Object o) { E[] a = this.a; if (o == null) { for (int i = 0; i < a.length; i++) if (a[i] == null) return i; } else { for (int i = 0; i < a.length; i++) if (o.equals(a[i])) return i; } return -1; |
可以看出最終呼叫的是物件的equals方法,所以當呼叫20行程式碼list.contains(b)時,實際上呼叫了
b.equals(a[i]),a[i]是集合中的元素集合中的型別而且為A型別(只新增了a物件),雖然B繼承了A,但此時
1 |
a[i] instanceof B |
結果為false,equals方法也就會返回false;而當呼叫23行程式碼list.contains(a)時,實際上呼叫了a.equal(a[i]),其中a[i]是集合中的元素而且為B型別(只新增了b物件),由於B型別肯定是A型別(B繼承了A),所以
1 |
a[i] instanceof A |
結果為true,equals方法也就會返回true,這就是整個過程。但很明顯結果是有問題的,因為我們的 list的泛型是A,而B又繼承了A,此時無論加入了a還是b,都屬於同種型別,所以無論是contains(a),還是contains(b)都應該返回true才算正常。而最終卻出現上面的結果,這就是因為重寫equals方法時沒遵守對稱性原則導致的結果,如果沒遵守傳遞性也同樣會造成上述的結果。當然這裡的解決方法也比較簡單,我們只要將B類的equals方法修改一下就可以了。
1 2 3 4 5 6 7 8 9 |
static class B extends A{ @Override public boolean equals(Object obj) { if(obj instanceof B){ return true; } return super.equals(obj); } } |
到此,我們也應該明白了重寫equals必須遵守幾點原則的重要性了。當然這裡不止是list,只要是java集合類或者java類庫中的其他方法,重寫equals不遵守5點原則的話,都可能出現意想不到的結果。
5.為什麼重寫equals()的同時還得重寫hashCode()
這個問題之前我也很好奇,不過最後還是在書上得到了比較明朗的解釋,當然這個問題主要是針對對映相關的操作(Map介面)。學過資料結構的同學都知道Map介面的類會使用到鍵物件的雜湊碼,當我們呼叫put方法或者get方法對Map容器進行操作時,都是根據鍵物件的雜湊碼來計算儲存位置的,因此如果我們對雜湊碼的獲取沒有相關保證,就可能會得不到預期的結果。在java中,我們可以使用hashCode()來獲取物件的雜湊碼,其值就是物件的儲存地址,這個方法在Object類中宣告,因此所有的子類都含有該方法。那我們先來認識一下hashCode()這個方法吧。hashCode的意思就是雜湊碼,也就是雜湊碼,是由物件匯出的一個整型值,雜湊碼是沒有規律的,如果x與y是兩個不同的物件,那麼x.hashCode()與y.hashCode()基本是不會相同的,下面通過String類的hashCode()計算一組雜湊碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
package com.zejian.test; 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方法計算出來的物件儲存地址,所以雜湊碼自然也就不同了。那麼我們該如何重寫出一個較好的hashCode方法呢,其實並不難,我們只要合理地組織物件的雜湊碼,就能夠讓不同的物件產生比較均勻的雜湊碼。例如下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 |
package com.zejian.test; public class Model { private String name; private double salary; private int sex; @Override public int hashCode() { return name.hashCode()+new Double(salary).hashCode() + new Integer(sex).hashCode(); } } |
上面的程式碼我們通過合理的利用各個屬性物件的雜湊碼進行組合,最終便能產生一個相對比較好的或者說更加均勻的雜湊碼,當然上面僅僅是個參考例子而已,我們也可以通過其他方式去實現,只要能使雜湊碼更加均勻(所謂的均勻就是每個物件產生的雜湊碼最好都不衝突)就行了。不過這裡有點要注意的就是java 7中對hashCode方法做了兩個改進,首先java釋出者希望我們使用更加安全的呼叫方式來返回雜湊碼,也就是使用null安全的方法Objects.hashCode(注意不是Object而是java.util.Objects)方法,這個方法的優點是如果引數為null,就只返回0,否則返回物件引數呼叫的hashCode的結果。Objects.hashCode 原始碼如下:
1 2 3 |
public static int hashCode(Object o) { return o != null ? o.hashCode() : 0; } |
因此我們修改後的程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
package com.zejian.test; import java.util.Objects; public class Model { private String name; private double salary; private int sex; @Override public int hashCode() { return Objects.hashCode(name)+new Double(salary).hashCode() + new Integer(sex).hashCode(); } } |
java 7還提供了另外一個方法java.util.Objects.hash(Object… objects),當我們需要組合多個雜湊值時可以呼叫該方法。進一步簡化上述的程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package com.zejian.test; import java.util.Objects; public class Model { private String name; private double salary; private int sex; // @Override // public int hashCode() { // return Objects.hashCode(name)+new Double(salary).hashCode() // + new Integer(sex).hashCode(); // } @Override public int hashCode() { return Objects.hash(name,salary,sex); } } |
好了,到此hashCode()該介紹的我們都說了,還有一點要說的如果我們提供的是一個數值型別的變數的話,那麼我們可以呼叫Arrays.hashCode()來計算它的雜湊碼,這個雜湊碼是由陣列元素的雜湊碼組成的。接下來我們迴歸到我們之前的問題,重寫equals方法時也必須重寫hashCode方法。在Java API文件中關於hashCode方法有以下幾點規定(原文來自java深入解析一書)。
- 在java應用程式執行期間,如果在equals方法比較中所用的資訊沒有被修改,那麼在同一個物件上多次呼叫hashCode方法時必須一致地返回相同的整數。如果多次執行同一個應用時,不要求該整數必須相同。
- 如果兩個物件通過呼叫equals方法是相等的,那麼這兩個物件呼叫hashCode方法必須返回相同的整數。
- 如果兩個物件通過呼叫equals方法是不相等的,不要求這兩個物件呼叫hashCode方法必須返回不同的整數。但是程式設計師應該意識到對不同的物件產生不同的hash值可以提供雜湊表的效能。
通過前面的分析,我們知道在Object類中,hashCode方法是通過Object物件的地址計算出來的,因為Object物件只與自身相等,所以同一個物件的地址總是相等的,計算取得的雜湊碼也必然相等,對於不同的物件,由於地址不同,所獲取的雜湊碼自然也不會相等。因此到這裡我們就明白了,如果一個類重寫了equals方法,但沒有重寫hashCode方法,將會直接違法了第2條規定,這樣的話,如果我們通過對映表(Map介面)操作相關物件時,就無法達到我們預期想要的效果。如果大家不相信, 可以看看下面的例子(來自java深入解析一書)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
package com.zejian.test; import java.util.HashMap; import java.util.Map; public class MapTest { public static void main(String[] args) { Map<String,Value> map1 = new HashMap<String,Value>(); String s1 = new String("key"); String s2 = new String("key"); Value value = new Value(2); map1.put(s1, value); System.out.println("s1.equals(s2):"+s1.equals(s2)); System.out.println("map1.get(s1):"+map1.get(s1)); System.out.println("map1.get(s2):"+map1.get(s2)); Map<Key,Value> map2 = new HashMap<Key,Value>(); Key k1 = new Key("A"); Key k2 = new Key("A"); map2.put(k1, value); System.out.println("k1.equals(k2):"+s1.equals(s2)); System.out.println("map2.get(k1):"+map2.get(k1)); System.out.println("map2.get(k2):"+map2.get(k2)); } /** * 鍵 * @author zejian * */ static class Key{ private String k; public Key(String key){ this.k=key; } @Override public boolean equals(Object obj) { if(obj instanceof Key){ Key key=(Key)obj; return k.equals(key.k); } return false; } } /** * 值 * @author zejian * */ static class Value{ private int v; public Value(int v){ this.v=v; } @Override public String toString() { return "類Value的值-->"+v; } } } |
程式碼比較簡單,我們就不過多解釋了(注意Key類並沒有重寫hashCode方法),直接執行看結果
1 2 3 4 5 6 |
s1.equals(s2):true map1.get(s1):類Value的值-->2 map1.get(s2):類Value的值-->2 k1.equals(k2):true map2.get(k1):類Value的值-->2 map2.get(k2):null |
對於s1和s2的結果,我們並不驚訝,因為相同的內容的s1和s2獲取相同內的value這個很正常,因為String類重寫了equals方法和hashCode方法,使其比較的是內容和獲取的是內容的雜湊碼。但是對於k1和k2的結果就不太盡人意了,k1獲取到的值是2,k2獲取到的是null,這是為什麼呢?想必大家已經發現了,Key只重寫了equals方法並沒有重寫hashCode方法,這樣的話,equals比較的確實是內容,而hashCode方法呢?沒重寫,那就肯定呼叫超類Object的hashCode方法,這樣返回的不就是地址了嗎?k1與k2屬於兩個不同的物件,返回的地址肯定不一樣,所以現在我們知道呼叫map2.get(k2)為什麼返回null了吧?那麼該如何修改呢?很簡單,我們要做也重寫一下hashCode方法即可(如果參與equals方法比較的成員變數是引用型別的,則可以遞迴呼叫hashCode方法來實現):
1 2 3 4 |
@Override public int hashCode() { return k.hashCode(); } |
再次執行:
1 2 3 4 5 6 |
s1.equals(s2):true map1.get(s1):類Value的值-->2 map1.get(s2):類Value的值-->2 k1.equals(k2):true map2.get(k1):類Value的值-->2 map2.get(k2):類Value的值-->2 |
6.重寫equals()中getClass與instanceof的區別
雖然前面我們都在使用instanceof(當然前面我們是根據需求(批次相同即相等)而使用instanceof的),但是在重寫equals() 方法時,一般都是推薦使用 getClass 來進行型別判斷(除非所有的子類有統一的語義才使用instanceof),不是使用 instanceof。我們都知道 instanceof 的作用是判斷其左邊物件是否為其右邊類的例項,返回 boolean 型別的資料。可以用來判斷繼承中的子類的例項是否為父類的實現。下來我們來看一個例子:父類Person
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
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屬性,而且這裡我們並沒有統一語義。測試程式碼如下:
1 2 3 4 5 6 7 8 9 10 |
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(除非子類擁有統一的語義)。
7.編寫一個完美equals()的幾點建議
下面給出編寫一個完美的equals方法的建議(出自Java核心技術 第一卷:基礎知識):
1)顯式引數命名為otherObject,稍後需要將它轉換成另一個叫做other的變數(引數名命名,強制轉換請參考建議5)
2)檢測this與otherObject是否引用同一個物件 :if(this == otherObject) return true;(儲存地址相同,肯定是同個物件,直接返回true)
3) 檢測otherObject是否為null ,如果為null,返回false.if(otherObject == null) return false;
4) 比較this與otherObject是否屬於同一個類 (視需求而選擇)
- 如果equals的語義在每個子類中有所改變,就使用getClass檢測 :if(getClass()!=otherObject.getClass()) return false; (參考前面分析的第6點)
- 如果所有的子類都擁有統一的語義,就使用instanceof檢測 :if(!(otherObject instanceof ClassName)) return false;(即前面我們所分析的父類car與子類bigCar混合比,我們統一了批次相同即相等)
5) 將otherObject轉換為相應的類型別變數:ClassName other = (ClassName) otherObject;
6) 現在開始對所有需要比較的域進行比較 。使用==比較基本型別域,使用equals比較物件域。如果所有的域都匹配,就返回true,否則就返回flase。
- 如果在子類中重新定義equals,就要在其中包含呼叫super.equals(other)
- 當此方法被重寫時,通常有必要重寫 hashCode 方法,以維護 hashCode 方法的常規協定,該協定宣告 相等物件必須具有相等的雜湊碼 。
參考資料:
Java核心技術 第一卷:基礎知識
Java深入分析
http://wiki.jikexueyuan.com/project/java-enhancement/java-thirteen.html
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!