深入理解Java的equals和hashCode方法
1、談談equals方法
相信大家對這個這個方法一定不陌生。該方法是Object基類里的非final方法(被設計成可覆蓋的),下面我們來看看Object中是如何實現該方法的。源代碼如下:
public boolean equals(Object obj) { return (this == obj); }
從源代碼我們知道,Object類默認為我們實現了通過比較兩個對象的內存地址是來判斷對象之間是否相等。即若 object1.equals(object2) 為 true,則表示 object1 和 object2 實際上是引用同一個對象。
2、equals方法和==操作符的異同
我們知道對于引用類型,==操作符比較的都是對象間的內存地址(對象的引用)。通過看Object類源碼知道,equals方法默認實現間接使用了==操作符。細心的同學可能會問,那我們想比較對象之間的內容呢?這時候我們可以通過覆蓋(override)equals方法來自定義比較邏輯。
簡單總結如下:
默認情況下也就是從超類Object繼承而來的equals方法與‘==’是完全等價的,比較的都是對象的內存地址,但我們可以重寫equals方法,使其按照我們需要的方式進行比較,如String類重寫了equals方法,比較的是字符的序列,而不再是內存地址。
3、equals方法的重寫規則
我們知道通過重寫equals方法可以自定義比較邏輯,重寫Equal方法是需要有相關規則的,下面我們來一一介紹。
- 自反性。對于任何非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個規則,我們來看下面這個例子:
接下來我們來看驗證這5條規則的代碼:
public static void main(String[] args) {Point point1=new Point(1, 1);Point point2=new Point(1, 1);Point point3=new Point(1, 1);//equals方法規則測試System.out.println("equals方法自反性:"+point1.equals(point1));System.out.println("equals方法對稱性:");System.out.println(point1.equals(point2));System.out.println(point2.equals(point1));System.out.println("equals方法傳遞性:");System.out.println(point1.equals(point2));System.out.println(point2.equals(point3));System.out.println(point1.equals(point3));System.out.println("equals方法一致性:");for(int i=0;i<5;i++) {System.out.println(point1.equals(point2));}System.out.println("null比較:");System.out.println(point1.equals(null));System.out.println(point2.equals(null));System.out.println(point3.equals(null)); } 復制代碼運行結果如下:
equals方法自反性:true
equals方法對稱性: true true
equals方法傳遞性: true true true
equals方法一致性: true true true true true
null比較: false false false
我們看到同一個類進行比較,理解起來不難。如果重寫equals方法存在繼承情況呢?下面我們就來詳細討論這個情況。
類繼承層次下的equals方法重寫的要點
我們還是以剛才PointClass為例,現在我們添加一個color字段,原來的Point類代碼保持不變,ColorPoint代碼如下:
class ColorPoint extends Point{private String color;public ColorPoint(int x,int y,String color) {super(x, y);this.color=color;}@Overridepublic boolean equals(Object obj) {if(!(obj instanceof ColorPoint)) {return false;}ColorPoint colorPoint=(ColorPoint)obj;return (super.equals(colorPoint) && this.color.equals(colorPoint.color));}@Overridepublic int hashCode() {return Objects.hash(super.hashCode(),this.color);} } 復制代碼我們在Main方法寫代碼測試下:
ColorPoint colorPoint=new ColorPoint(1, 1, "#ffffff");Point point=new Point(1, 1);System.out.println("對稱性:");System.out.println(colorPoint.equals(point)); //falseSystem.out.println(point.equals(colorPoint)); //true 復制代碼我們看到第2個條件對稱性現在就無法滿足了。我們稍稍分析下,第一個比較方法返回false,很好理解,因為point實例不是ClorPoint類型,所以在ColorPoint類equals方法里面if(!(obj instanceof ColorPoint))成立,返回false。第二個比較方法執行的是Point類的equals方法,會正常返回true。如果現在我們有特殊需求,兩個對象只要是x,y一致,我們就認為是一樣的對象。顯然,上面的這種重寫方式是不對的。 這種比較方法問題在于,colorPoint.equals(point)總是會返回false。那如何解決呢?聰明的同學想到了這種方法:
@Override public boolean equals(Object obj) {if(!(obj instanceof Point)) {return false;}if(!(obj instanceof ColorPoint)) {return obj.equals(this);}ColorPoint colorPoint=(ColorPoint)obj;return (super.equals(colorPoint) && this.color.equals(colorPoint.color)); } 復制代碼這個方法似乎符合要求,這個方法首先判斷obj是否是Point類及其子類,如果不是,那么直接返回false。否則的話將比較訪問縮小,看是否是ColorPoint及其子類,如果不是的話,說明至少是ColorPoint的基類,就可以使用它equals方法進行比較,否則使用ColorPoint類的equals方法比較。
但是這個寫法不符合傳遞性規則,我們舉例來看:
出現這種情況的根本原因在于:
-
父類與子類進行混合比較。
-
子類中聲明了新變量,并且在子類equals方法使用了新增的成員變量作為判斷對象是否相等的條件。
只要滿足上面兩個條件,傳遞性就會失效。Effective Java中第8條對這個有一個總結:這是面向對象語言中關于等價關系的一個基本問題,我們無法再擴展可實例化的類的同時,既增加新的值組件,同時又保留equals約定,除非愿意放棄面向對象帶來的優勢。 沒有直接的方法解決這個問題,間接的方法還是有的,不要使用繼承結構,使用組合的方式。我們將上面的ColorPoint類進行改寫。代碼如下:
重寫equals是getClass和instanceof關鍵字的區別
我們前面在重寫equals方法時,使用的都是instanceof方法,但是重寫equals時推薦的還是使用getClass來進行類型判斷。什么情況下可以使用instanceof關鍵字,除非父類和子類有統一的語義。下面舉一個例子給大家看下:
首先是:父類Person
子類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;}public boolean equals(Object object){if(object instanceof Employee){Employee e = (Employee) object;return super.equals(object) && e.getId() == id;}return false;} } 復制代碼我們來看測試代碼:
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)); //trueSystem.out.println(p1.equals(e2)); //trueSystem.out.println(e1.equals(e2)); //false } 復制代碼上面代碼我們定義了兩個員工和一個普通人,雖然他們同名,但是他們肯定不是同一人,所以按理來說結果應該全部是 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(除非子類equals方法擁有統一的語義,例如繼承Point類的子類中,我們認為只需要x,y相等的兩個實例就認為是相等的)。
編寫一個高質量equals方法的幾點建議
- 使用==操作符檢查“參數是否為這個對象的引用”,如果是,返回true,這只是一種性能優化的寫法。
- 如果equals方法的語義在每一個子類中有所改變,就使用getClass來判斷類型。如果所有的子類都擁有統一的語義(例如繼承Point類的子類中,我們認為只需要x,y相等的兩個實例就認為是相等的)我們可以使用instanceof來判斷類型。
- 對于該類中的每個“關鍵”域,檢查參數中的域是否與該對象中的對應的域相匹配。如果測試全部成功,返回true,否則返回false。
- 當你編寫完equals返回之后,應該確認它是否是對稱的、可傳遞的、一致的。
- 覆蓋equals時總要覆蓋hashCode
4、不符合規則的equals方法帶來的影響
前面說的都是如何寫出符合規范的equals方法,那么我們寫的equals方法如果不符合5條規范,到底會有什么影響呢?我們通過例子來看一下
我們看上面的A,B兩個類equals方法是不符合對稱性的。我們看到我們將其放入a的時候list.contains(b)返回false。當我們放入b的時候list.contains(a)又返回true了,那我們來看看contains返回到底執行了什么。代碼如下:
我們先來看第一組比較過程,list里面現在有a對象,根據contains源碼(o.equals(elementData[i])),我們知道它會執行這兩種代碼a.equals(a)和b.equals(a)。結果當然是true和false。接下來我們來看第二組比較過程,這時候list里面保存的是b對象。兩次的比較過程分別是b.equals(b)和a.equals(b)。結果當然會是true和true。 很顯然,上面的重寫equals方法是存在問題的。它沒有遵循對稱性原則。我們只需要修改B類的equals方法即可。
class B extends A{@Overridepublic boolean equals(Object obj) {if(obj instanceof B){return true;}return super.equals(obj);} } 復制代碼簡單的總結:只要是java集合類或者java類庫中的其他方法,重寫equals不遵守5點原則的話,都可能出現意想不到的結果。
5、重寫equals方法的時候必須也重寫hashCode方法
前面介紹正確重寫equals方法有很大一部分原因是為了配合JDK集合類使用的時候不出現問題(集合類的很多操作都是依賴于正確實現equals方法的)。重寫hashCode方法是為了類實例在Map集合中使用的時候可以產生正確的行為。
學過數據結構的同學都知道Map接口的類會使用到鍵對象的哈希碼,當我們調用put方法或者get方法對Map容器進行操作時,都是根據鍵對象的哈希碼來計算存儲位置的,因此如果我們對哈希碼的獲取沒有相關保證,就可能會得不到預期的結果。在java中,我們可以使用hashCode()來獲取對象的哈希碼,其值就是對象的存儲地址(默認實現),這個方法在Object類中聲明,因此所有的子類都含有該方法。hashCode的意思就是散列碼,也就是哈希碼,是由對象導出的一個整型值,散列碼是沒有規律的,如果x與y是兩個不同的對象,那么x.hashCode()與y.hashCode()基本是不會相同的。那么equals方法和hashCode方法到底有什么關聯呢?我們來詳細的看下,相關規則如下:
通過前面的分析,我們知道在Object類中,hashCode方法是通過Object對象的地址計算出來的,因為Object對象只與自身相等,所以同一個對象的地址總是相等的,計算取得的哈希碼也必然相等,對于不同的對象,由于地址不同,所獲取的哈希碼自然也不會相等。因此到這里我們就明白了,如果一個類重寫了equals方法,但沒有重寫hashCode方法,將會直接違法了第2條規定,這樣的話,如果我們通過映射表(Map接口)操作相關對象時,就無法達到我們預期想要的效果,下面我們通過一個例子來看一下:
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));}/*** 鍵*/class Key{private String k;public Key(String key){this.k=key;}@Overridepublic boolean equals(Object obj) {if(obj instanceof Key){Key key=(Key)obj;return k.equals(key.k);}return false;}}/*** 值*/static class Value{private int v;public Value(int v){this.v=v;}@Overridepublic String toString() {return "類Value的值-->"+v;}} 復制代碼
運行結果如下:
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方法來實現):
@Override public int hashCode() {return k.hashCode(); } 復制代碼6、參考資料
特別感謝下列博客/書籍:
https://blog.csdn.net/javazejian/article/details/51348320
書籍:《Effective Java》 書籍:《Java編程思想》
轉載于:https://juejin.im/post/5b4f4e0451882519ee7fc3d8
總結
以上是生活随笔為你收集整理的深入理解Java的equals和hashCode方法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python---------sys.a
- 下一篇: 数据结构p91