為什么要同時重寫equals和hashcode


原文地址https://blog.csdn.net/tiantiandjava/article/details/46988461

原文地址https://blog.csdn.net/lijiecao0226/article/details/24609559

知乎文章https://www.zhihu.com/question/26872848  這篇直呼文章很重要!!!!-------------------- ==,equals的區別

首先來區分Java中 ==,equals,hashCode的區別

==是運算符,用於比較兩個變量是否相等。

equals,是Objec類的方法,用於比較兩個對象是否相等,默認Object類的equals方法是比較兩個對象的地址,跟==的結果一樣。Object的equals方法如下:

 

[java]  view plain  copy
 
  1. public boolean equals(Object obj) {  
  2.     return (this == obj);  
  3. }  


hashCode也是Object類的一個方法。返回一個離散的int型整數。在集合類操作中使用,為了提高查詢速度。(HashMap,HashSet等)

 

 

有了這三個基礎概念,區別就簡單了。網上有很多,匯總一下:

java中的數據類型,可分為兩類: 
1.基本數據類型,也稱原始數據類型。byte,short,char,int,long,float,double,boolean 
  他們之間的比較,應用雙等號(==),比較的是他們的值。 
2.復合數據類型(類) 
  當他們用(==)進行比較的時候,比較的是他們在內存中的存放地址,所以,除非是同一個new出來的對象,他們的比較后的結果為true,否則比較后結果為false。 JAVA當中所有的類都是繼承於Object這個基類的,在Object中的基類中定義了一個equals的方法,這個方法的初始行為是比較對象的內存地 址,但在一些類庫當中這個方法被覆蓋掉了,如String,Integer,Date在這些類當中equals有其自身的實現,而不再是比較類在堆內存中的存放地址了。
  對於復合數據類型之間進行equals比較,在沒有覆寫equals方法的情況下,他們之間的比較還是基於他們在內存中的存放位置的地址值的,因為Object的equals方法也是用雙等號(==)進行比較的,所以比較后的結果跟雙等號(==)的結果相同。

 

如果兩個對象根據equals()方法比較是相等的,那么調用這兩個對象中任意一個對象的hashCode方法都必須產生同樣的整數結果。
如果兩個對象根據equals()方法比較是不相等的,那么調用這兩個對象中任意一個對象的hashCode方法,則不一定要產生相同的整數結果

從而在集合操作的時候有如下規則:

將對象放入到集合中時,首先判斷要放入對象的hashcode值與集合中的任意一個元素的hashcode值是否相等,如果不相等直接將該對象放入集合中。如果hashcode值相等,然后再通過equals方法判斷要放入對象與集合中的任意一個對象是否相等,如果equals判斷不相等,直接將該元素放入到集合中,否則不放入。

回過來說get的時候,HashMap也先調key.hashCode()算出數組下標,然后看equals如果是true就是找到了,所以就涉及了equals。

 

 

《Effective Java》書中有兩條是關於equals和hashCode的:

覆蓋equals時需要遵守的通用約定: 
  覆蓋equals方法看起來似乎很簡單,但是如果覆蓋不當會導致錯誤,並且后果相當嚴重。《Effective Java》一書中提到“最容易避免這類問題的辦法就是不覆蓋equals方法”,這句話貌似很搞笑,其實想想也不無道理,其實在這種情況下,類的每個實例都只與它自身相等。如果滿足了以下任何一個條件,這就正是所期望的結果: 
類的每個實例本質上都是唯一的。對於代表活動實體而不是值的類來說卻是如此,例如Thread。Object提供的equals實現對於這些類來說正是正確的行為。
不關心類是否提供了“邏輯相等”的測試功能。假如Random覆蓋了equals,以檢查兩個Random實例是否產生相同的隨機數序列,但是設計者並不認為客戶需要或者期望這樣的功能。在這樣的情況下,從Object繼承得到的equals實現已經足夠了。
超類已經覆蓋了equals,從超類繼承過來的行為對於子類也是合適的。大多數的Set實現都從AbstractSet繼承equals實現,List實現從AbstractList繼承equals實現,Map實現從AbstractMap繼承equals實現。
類是私有的或者是包級私有的,可以確定它的equals方法永遠不會被調用。在這種情況下,無疑是應該覆蓋equals方法的,以防止它被意外調用:
@Override 
public boolean equals(Object o){ 
  throw new AssertionError(); //Method is never called 


  在覆蓋equals方法的時候,你必須要遵守它的通用約定。下面是約定的內容,來自Object的規范[JavaSE6] 
自反性。對於任何非null的引用值x,x.equals(x)必須返回true。
對稱性。對於任何非null的引用值x和y,當且僅當y.equals(x)返回true時,x.equals(y)必須返回true
傳遞性。對於任何非null的引用值x、y和z,如果x.equals(y)返回true,並且y.equals(z)也返回true,那么x.equals(z)也必須返回true。
一致性。對於任何非null的引用值x和y,只要equals的比較操作在對象中所用的信息沒有被修改,多次調用該x.equals(y)就會一直地返回true,或者一致地返回false。
對於任何非null的引用值x,x.equals(null)必須返回false。

  結合以上要求,得出了以下實現高質量equals方法的訣竅: 
1.使用==符號檢查“參數是否為這個對象的引用”。如果是,則返回true。這只不過是一種性能優化,如果比較操作有可能很昂貴,就值得這么做。 
2.使用instanceof操作符檢查“參數是否為正確的類型”。如果不是,則返回false。一般來說,所謂“正確的類型”是指equals方法所在的那個類。 
3.把參數轉換成正確的類型。因為轉換之前進行過instanceof測試,所以確保會成功。 
4.對於該類中的每個“關鍵”域,檢查參數中的域是否與該對象中對應的域相匹配。如果這些測試全部成功,則返回true;否則返回false。 
5.當編寫完成了equals方法之后,檢查“對稱性”、“傳遞性”、“一致性”。 

 

覆蓋equals時總要覆蓋hashCode 的原因:
  一個很常見的錯誤根源在於沒有覆蓋hashCode方法。在每個覆蓋了equals方法的類中,也必須覆蓋hashCode方法。如果不這樣做的話,就會違反Object.hashCode的通用約定,從而導致該類無法結合所有基於散列的集合一起正常運作,這樣的集合包括HashMap、HashSet和Hashtable。

比如將兩個對象存入HashSet里面:如果不重寫hashCode,還用原來的hashCode就可能判斷兩個對象的hashCode不相等(其實重寫hashCode再判斷這兩個對象才發現這兩個對象的hashCode是相等的),然后因為判斷這兩個對象的hashCode不相等就會直接將這兩個對象存入HashSet,這就導致了實際上存入了這兩個相同的對象。


在應用程序的執行期間,只要對象的equals方法的比較操作所用到的信息沒有被修改,那么對這同一個對象調用多次,hashCode方法都必須始終如一地返回同一個整數。在同一個應用程序的多次執行過程中,每次執行所返回的整數可以不一致。
如果兩個對象根據equals()方法比較是相等的,那么調用這兩個對象中任意一個對象的hashCode方法都必須產生同樣的整數結果。
如果兩個對象根據equals()方法比較是不相等的,那么調用這兩個對象中任意一個對象的hashCode方法,則不一定要產生相同的整數結果。但是程序員應該知道,給不相等的對象產生截然不同的整數結果,有可能提高散列表的性能。

 

========================================================================================================================

========================================================================================================================

========================================================================================================================

========================================================================================================================

 

                                                                                      最好同時重寫equals和hashcode的原因

最近去面試了幾家公司,被問到hashCode的作用,雖然回答出來了,但是自己還是對hashCode和equals的作用一知半解的,所以決定把它們研究一下。

以前寫程序一直沒有注意hashCode的作用,一般都是覆蓋了equals,缺沒有覆蓋hashCode,現在發現這是埋下了很多潛在的Bug!今天就來說一說hashCode和equals的作用。

       先來試想一個場景,如果你想查找一個集合中是否包含某個對象,那么程序應該怎么寫呢?通常的做法是逐一取出每個元素與要查找的對象一一比較,當發現兩者進行equals比較結果相等時,則停止查找並返回true,否則,返回false。但是這個做法的一個缺點是當集合中的元素很多時,譬如有一萬個元素,那么逐一的比較效率勢必下降很快。於是有人發明了一種哈希算法來提高從該集合中查找元素的效率,這種方式將集合分成若干個存儲區域(可以看成一個個桶),每個對象可以計算出一個哈希碼,可以根據哈希碼分組,每組分別對應某個存儲區域,這樣一個對象根據它的哈希碼就可以分到不同的存儲區域(不同的桶中)。如下圖所示:

                                            

實際的使用中,一個對象一般有key和value,可以根據key來計算它的hashCode。假設現在全部的對象都已經根據自己的hashCode值存儲在不同的存儲區域中了,那么現在查找某個對象(根據對象的key來查找),不需要遍歷整個集合了,現在只需要計算要查找對象的key的hashCode,然后找到該hashCode對應的存儲區域,在該存儲區域中來查找就可以了,這樣效率也就提升了很多。說了這么多相信你對hashCode的作用有了一定的了解,下面就來看看hashCode和equals的區別和聯系。

在研究這個問題之前,首先說明一下JDK對equals(Object obj)和hashCode()這兩個方法的定義和規范:在Java中任何一個對象都具備equals(Object obj)和hashCode()這兩個方法,因為他們是在Object類中定義的。 equals(Object obj)方法用來判斷兩個對象是否“相同”,如果“相同”則返回true,否則返回false。 hashCode()方法返回一個int數,在Object類中的默認實現是“將該對象的內部地址轉換成一個整數返回”。 

下面是官方文檔給出的一些說明:

 

[java]  view plain  copy
 
  1. <span style="font-size:18px;">hashCode 的常規協定是:     
  2. 在 Java 應用程序執行期間,在同一對象上多次調用 hashCode 方法時,必須一致地返回相同的整數,前提是對象上 equals 比較中所用的信息沒有被修改。從某一應用程序的一次執行到同一應用程序的另一次執行,該整數無需保持一致。     
  3. 如果根據 equals(Object) 方法,兩個對象是相等的,那么在兩個對象中的每個對象上調用 hashCode 方法都必須生成相同的整數結果。     
  4. 以下情況不 是必需的:如果根據 equals(java.lang.Object) 方法,兩個對象不相等,那么在兩個對象中的任一對象上調用 hashCode 方法必定會生成不同的整數結果。但是,程序員應該知道,為不相等的對象生成不同整數結果可以提高哈希表的性能。     
  5. 實際上,由 Object 類定義的 hashCode 方法確實會針對不同的對象返回不同的整數。(這一般是通過將該對象的內部地址轉換成一個整數來實現的,但是 JavaTM 編程語言不需要這種實現技巧。)     
  6.     
  7. 當equals方法被重寫時,通常有必要重寫 hashCode 方法,以維護 hashCode 方法的常規協定,該協定聲明相等對象必須具有相等的哈希碼。</span>  


下面是我查閱了相關資料之后對以上的說明做的歸納總結:

 

1.若重寫了equals(Object obj)方法,則有必要重寫hashCode()方法。

2.若兩個對象equals(Object obj)返回true,則hashCode()有必要也返回相同的int數。

3.若兩個對象equals(Object obj)返回false,則hashCode()不一定返回不同的int數。

4.若兩個對象hashCode()返回相同int數,則equals(Object obj)不一定返回true。

5.若兩個對象hashCode()返回不同int數,則equals(Object obj)一定返回false。

6.同一對象在執行期間若已經存儲在集合中,則不能修改影響hashCode值的相關信息,否則會導致內存泄露問題。

 
想要弄清楚以上六點,先要知道什么時候需要重寫equals和hashCode。一般來說涉及到對象之間的比較大小就需要重寫equals方法,但是為什么第一點說重寫了equals就需要重寫hashCode呢?實際上這只是一條規范,如果不這樣做程序也可以執行,只不過會隱藏bug。一般一個類的對象如果會存儲在HashTable,HashSet,HashMap等散列存儲結構中,那么重寫equals后最好也重寫hashCode,否則會導致存儲數據的不唯一性(存儲了兩個equals相等的數據)。而如果確定不會存儲在這些散列結構中,則可以不重寫hashCode。但是個人覺得還是重寫比較好一點,誰能保證后期不會存儲在這些結構中呢,況且重寫了hashCode也不會降低性能,因為在線性結構(如ArrayList)中是不會調用hashCode,所以重寫了也不要緊,也為后期的修改打了補丁。
 
下面來看一張對象放入散列集合的流程圖:

 

從上面的圖中可以清晰地看到在存儲一個對象時,先進行hashCode值的比較,然后進行equals的比較。可能現在你已經對上面的6點歸納有了一些認識。我們還可以通過JDK中得源碼來認識一下具體hashCode和equals在代碼中是如何調用的。

HashSet.java 

 

[java]  view plain  copy
 
  1. <span style="font-size:18px;">  public boolean add(E e) {  
  2.     return map.put(e, PRESENT)==null;  
  3.     }</span>  

HashMap.java

 

[java]  view plain  copy
 
  1. <span style="font-size:18px;">    public V put(K key, V value) {  
  2.         if (key == null)  
  3.             return putForNullKey(value);  
  4.         int hash = hash(key.hashCode());  
  5.         int i = indexFor(hash, table.length);  
  6.         for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
  7.             Object k;  
  8.             if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
  9.                 V oldValue = e.value;  
  10.                 e.value = value;  
  11.                 e.recordAccess(this);  
  12.                 return oldValue;  
  13.             }  
  14.         }  
  15.   
  16.         modCount++;  
  17.         addEntry(hash, key, value, i);  
  18.         return null;  
  19.     }</span>  


 

 

最后再來看幾個測試的例子吧:

測試一:覆蓋equals(Object obj)但不覆蓋hashCode(),導致數據不唯一性

 

[java]  view plain  copy
 
  1. <span style="font-size:18px;">public class HashCodeTest {  
  2.     public static void main(String[] args) {  
  3.         Collection set = new HashSet();  
  4.         Point p1 = new Point(1, 1);  
  5.         Point p2 = new Point(1, 1);  
  6.   
  7.         System.out.println(p1.equals(p2));  
  8.         set.add(p1);   //(1)  
  9.         set.add(p2);   //(2)  
  10.         set.add(p1);   //(3)  
  11.   
  12.         Iterator iterator = set.iterator();  
  13.         while (iterator.hasNext()) {  
  14.             Object object = iterator.next();  
  15.             System.out.println(object);  
  16.         }  
  17.     }  
  18. }  
  19.   
  20. class Point {  
  21.     private int x;  
  22.     private int y;  
  23.   
  24.     public Point(int x, int y) {  
  25.         super();  
  26.         this.x = x;  
  27.         this.y = y;  
  28.     }  
  29.   
  30.     @Override  
  31.     public boolean equals(Object obj) {  
  32.         if (this == obj)  
  33.             return true;  
  34.         if (obj == null)  
  35.             return false;  
  36.         if (getClass() != obj.getClass())  
  37.             return false;  
  38.         Point other = (Point) obj;  
  39.         if (x != other.x)  
  40.             return false;  
  41.         if (y != other.y)  
  42.             return false;  
  43.         return true;  
  44.     }  
  45.   
  46.     @Override  
  47.     public String toString() {  
  48.         return "x:" + x + ",y:" + y;  
  49.     }  
  50.   
  51. }  
  52. </span>  



 

輸出結果:

 

[java]  view plain  copy
 
  1. <span style="font-size:18px;">true  
  2. x:1,y:1  
  3. x:1,y:1  
  4. </span>  


原因分析:

 

(1)當執行set.add(p1)時(1),集合為空,直接存入集合;

(2)當執行set.add(p2)時(2),首先判斷該對象(p2)的hashCode值所在的存儲區域是否有相同的hashCode,因為沒有覆蓋hashCode方法,所以jdk使用默認Object的hashCode方法,返回內存地址轉換后的整數,因為不同對象的地址值不同,所以這里不存在與p2相同hashCode值的對象,因此jdk默認不同hashCode值,equals一定返回false,所以直接存入集合。

 (3)當執行set.add(p1)時(3),時,因為p1已經存入集合,同一對象返回的hashCode值是一樣的,繼續判斷equals是否返回true,因為是同一對象所以返回true。此時jdk認為該對象已經存在於集合中,所以舍棄。

 

測試二:覆蓋hashCode方法,但不覆蓋equals方法,仍然會導致數據的不唯一性

修改Point類:

[java]  view plain  copy
 
  1. <span style="font-size:18px;">class Point {  
  2.     private int x;  
  3.     private int y;  
  4.   
  5.     public Point(int x, int y) {  
  6.         super();  
  7.         this.x = x;  
  8.         this.y = y;  
  9.     }  
  10.   
  11.     @Override  
  12.     public int hashCode() {  
  13.         final int prime = 31;  
  14.         int result = 1;  
  15.         result = prime * result + x;  
  16.         result = prime * result + y;  
  17.         return result;  
  18.     }  
  19.   
  20.     @Override  
  21.     public String toString() {  
  22.         return "x:" + x + ",y:" + y;  
  23.     }  
  24.   
  25. }  
  26. </span>  


輸出結果:

[java]  view plain  copy
 
  1. <span style="font-size:18px;">false  
  2. x:1,y:1  
  3. x:1,y:1</span>  

 

 

原因分析:

(1)當執行set.add(p1)時(1),集合為空,直接存入集合;

(2)當執行set.add(p2)時(2),首先判斷該對象(p2)的hashCode值所在的存儲區域是否有相同的hashCode,這里覆蓋了hashCode方法,p1和p2的hashCode相等,所以繼續判斷equals是否相等,因為這里沒有覆蓋equals,默認使用'=='來判斷(返回內存地址),所以這里equals返回false,jdk認為是不同的對象,所以將p2存入集合。

 (3)當執行set.add(p1)時(3),時,因為p1已經存入集合,同一對象返回的hashCode值是一樣的,並且equals返回true。此時jdk認為該對象已經存在於集合中,所以舍棄。

 

綜合上述兩個測試,要想保證元素的唯一性,必須同時覆蓋hashCode和equals才行。
(注意:在HashSet中插入同一個元素(hashCode和equals均相等)時,會被舍棄,而在HashMap中插入同一個Key(Value 不同)時,原來的元素會被覆蓋。)
 
測試三:在內存泄露問題
[java]  view plain  copy
 
  1. <span style="font-size:18px;">public class HashCodeTest {  
  2.     public static void main(String[] args) {  
  3.         Collection set = new HashSet();  
  4.         Point p1 = new Point(1, 1);  
  5.         Point p2 = new Point(1, 2);  
  6.   
  7.         set.add(p1);  
  8.         set.add(p2);  
  9.           
  10.         p2.setX(10);  
  11.         p2.setY(10);  
  12.           
  13.         set.remove(p2);  
  14.   
  15.         Iterator iterator = set.iterator();  
  16.         while (iterator.hasNext()) {  
  17.             Object object = iterator.next();  
  18.             System.out.println(object);  
  19.         }  
  20.     }  
  21. }  
  22.   
  23. class Point {  
  24.     private int x;  
  25.     private int y;  
  26.   
  27.     public Point(int x, int y) {  
  28.         super();  
  29.         this.x = x;  
  30.         this.y = y;  
  31.     }  
  32.   
  33.   
  34.     public int getX() {  
  35.         return x;  
  36.     }  
  37.   
  38.   
  39.     public void setX(int x) {  
  40.         this.x = x;  
  41.     }  
  42.   
  43.   
  44.     public int getY() {  
  45.         return y;  
  46.     }  
  47.   
  48.   
  49.     public void setY(int y) {  
  50.         this.y = y;  
  51.     }  
  52.   
  53.   
  54.     @Override  
  55.     public int hashCode() {  
  56.         final int prime = 31;  
  57.         int result = 1;  
  58.         result = prime * result + x;  
  59.         result = prime * result + y;  
  60.         return result;  
  61.     }  
  62.   
  63.   
  64.     @Override  
  65.     public boolean equals(Object obj) {  
  66.         if (this == obj)  
  67.             return true;  
  68.         if (obj == null)  
  69.             return false;  
  70.         if (getClass() != obj.getClass())  
  71.             return false;  
  72.         Point other = (Point) obj;  
  73.         if (x != other.x)  
  74.             return false;  
  75.         if (y != other.y)  
  76.             return false;  
  77.         return true;  
  78.     }  
  79.   
  80.   
  81.     @Override  
  82.     public String toString() {  
  83.         return "x:" + x + ",y:" + y;  
  84.     }  
  85.   
  86. }  
  87. </span>  
 
運行結果:
[java]  view plain  copy
 
  1. <span style="font-size:18px;">x:1,y:1  
  2. x:10,y:10</span>  
 
原因分析:
    假設p1的hashCode為1,p2的hashCode為2,在存儲時p1被分配在1號桶中,p2被分配在2號筒中。這時修改了p2中與計算hashCode有關的信息(x和y),當調用remove(Object obj)時,首先會查找該hashCode值得對象是否在集合中。假設修改后的hashCode值為10(仍存在2號桶中),這時查找結果空,jdk認為該對象不在集合中,所以不會進行刪除操作。然而用戶以為該對象已經被刪除,導致該對象長時間不能被釋放,造成內存泄露。解決該問題的辦法是不要在執行期間修改與hashCode值有關的對象信息,如果非要修改,則必須先從集合中刪除,更新信息后再加入集合中。
 
總結:
   1.hashCode是為了提高在散列結構存儲中查找的效率,在線性表中沒有作用。
   2.equals和hashCode需要同時覆蓋。
   3.若兩個對象equals返回true,則hashCode有必要也返回相同的int數。

4.若兩個對象equals返回false,則hashCode不一定返回不同的int數,但為不相等的對象生成不同hashCode值可以提高 哈希表的性能。

5.若兩個對象hashCode返回相同int數,則equals不一定返回true。

6.若兩個對象hashCode返回不同int數,則equals一定返回false。

   7.同一對象在執行期間若已經存儲在集合中,則不能修改影響hashCode值的相關信息,否則會導致內存泄露問題。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM