1. 程式人生 > >hashCode與equals的作用與區別及應當註意的細節

hashCode與equals的作用與區別及應當註意的細節

實現 是個 發現 string 有關 一般來說 del @override 用戶

先來試想一個場景,如果你想查找一個集合中是否包含某個對象,那麽程序應該怎麽寫呢?通常的做法是逐一取出每個元素與要查找的對象一一比較,當發現兩者進行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類中的默認實現是“將該對象的內部地址轉換成一個整數返回”。

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

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

下面是我查閱了相關資料之後對以上的說明做的歸納總結:
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

1   public boolean add(E e) {
2     return map.put(e, PRESENT)==null;
3     }

HashMap.java

 1     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     }

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

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

 1 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 }

輸出結果:

1 true
2 x:1,y:1
3 x:1,y:1

原因分析:
(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類:

 1 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 }

輸出結果:

false
x:1,y:1
x:1,y:1

原因分析:

(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 不同)時,原來的元素會被覆蓋。) 測試三:在內存泄露問題
 1 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 }

運行結果:

1 x:1,y:1
2 x:10,y:10

原因分析:
假設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值的相關信息,否則會導致內存泄露問題。



hashCode與equals的作用與區別及應當註意的細節