爲什麼重寫equals方法的同時也要重寫hashcode方法?

參考鏈接:

http://www.iteye.com/problems/23334

http://www.iteye.com/topic/257191

第一個鏈接

首先說建議的情況:  比如你的對象想放到Set集合或者是想作爲Map的key時(非散列的Set和Map,例如TreeSet,TreeMap等),那麼你必須重寫equals()方法,這樣才能保證唯一性。當然,在這種情況下,你不想重寫hashCode()方法,也沒有錯。但是,對於良好的編程風格而言,你應該在重寫equals()方法的同時,也重寫hashCode()方法。 

然後再說說必須重寫hashCode()的情況: 
    如果你的對象想放進散列存儲的集合中(比如:HashSet,LinkedHashSet)或者想作爲散列Map(例如:HashMap,LinkedHashMap等等)的Key時,在重寫equals()方法的同時,必須重寫hashCode()方法。 

這裏我想給樓主講講sun的設計者爲什麼需要設計hashcode方法,也許這樣你就應該知道什麼時候該重寫它了。 
數據結構有一種爲了提高查找的效率而存在的數據結構——散列表,散列表其實是普通數組概念的推廣,因爲可以對數組進行直接尋址,故可以再O(1)時間內訪問數組的任意元素,thinking in java中有個對hashmap簡單的實現,我們來看看你就明白了: 

Java代碼  收藏代碼
  1. //: containers/SimpleHashMap.java  
  2. // A demonstration hashed Map.  
  3. import java.util.*;  
  4. import net.mindview.util.*;  
  5.   
  6. public class SimpleHashMap<K,V> extends AbstractMap<K,V> {  
  7.   // Choose a prime number for the hash table  
  8.   // size, to achieve a uniform distribution:  
  9.   static final int SIZE = 997;  
  10.   // You can't have a physical array of generics,  
  11.   // but you can upcast to one:  
  12.   @SuppressWarnings("unchecked")  
  13.   LinkedList<MapEntry<K,V>>[] buckets =  
  14.     new LinkedList[SIZE];[color=red]//List數組裏每項是個List,數組下標是hashcode方法的返回值再經過散列函數得到的,相當於散列表的關鍵字,它亦代表着每個對象的關鍵字。(不能顯示new一個泛型數組,但是你可以new一個泛型數組的引用,如有需要以後可以將普通數組轉型爲泛型數組)。[/color]  
  15.   public V put(K key, V value) {[color=red]//將這個對鍵值放進hashmap中。[/color]  
  16.     V oldValue = null;  
  17.     int index = Math.abs(key.hashCode()) % SIZE;[color=red]//這裏就是通過散列函數對hashcode的返回值處理得到一個關鍵字,它代表了對象在數組裏的位置,既是數組下標。[/color]  
  18.     if(buckets[index] == null)  
  19.       buckets[index] = new LinkedList<MapEntry<K,V>>();[color=red]//如果是第一次散列到這個數組下標,那麼就新生成一個LinkedList,可以看到它裏面保存的是MapEntry<K,V>鍵和值。[/color]  
  20.     LinkedList<MapEntry<K,V>> bucket = buckets[index];[color=red]//將這個LinkedList賦值給一個bucket(桶),以後就直接在這個bucket進行操作。[/color]  
  21.     MapEntry<K,V> pair = new MapEntry<K,V>(key, value);  
  22.     boolean found = false;  
  23.     ListIterator<MapEntry<K,V>> it = bucket.listIterator();  
  24.     while(it.hasNext()) {  
  25.       MapEntry<K,V> iPair = it.next();  
  26.       if(iPair.getKey().equals(key)) {[color=red]//如果已經存在同一個鍵值,那麼就更新value。[/color]  
  27.         oldValue = iPair.getValue();  
  28.         it.set(pair); // Replace old with new  
  29.         found = true;  
  30.         break;  
  31.       }  
  32.     }  
  33.     if(!found)  
  34.       buckets[index].add(pair);[color=red]//如果是一個新的鍵值,那麼直接添加到這個LinkedList中。[/color]  
  35.     return oldValue;  
  36.   }  
  37.   public V get(Object key) {[color=red]//看hashmap是如何憑藉hashcode方法快速定位到鍵值。[/color]  
  38.     int index = Math.abs(key.hashCode()) % SIZE;[color=red]//與put方法中的作用一樣,生成數組下標,因爲我存的時候就是存到這個地方的,那麼我取的時候直接訪問buckets[index]。[/color]  
  39.     if(buckets[index] == nullreturn null;[color=red]//直接訪問這個數組下標的LinkedList,如果爲null,則返回。[/color]  
  40.     for(MapEntry<K,V> iPair : buckets[index])[color=red]//爲什麼要用LinkedList,因爲hashcode方法產生的散列碼不能完全確定一個對象,也就是說會和其他對象發生“碰撞”,即散列到同一個數組下標,解決這個問題的組號辦法就是定義一個List把它們保存起來,但是在這個List中,我們必須保證能用equals方法確定對象的身份,這也就是爲什麼很多人說hashcode()相等,equals()不一定相等,而equals()相等的兩個對象,hashcode()一定相等。所以這裏要直接在LinkedList執行線性查找。[/color]  
  41.       if(iPair.getKey().equals(key))  
  42.         return iPair.getValue();  
  43.     return null;  
  44.   }  
  45.   public Set<Map.Entry<K,V>> entrySet() {  
  46.     Set<Map.Entry<K,V>> set= new HashSet<Map.Entry<K,V>>();  
  47.     for(LinkedList<MapEntry<K,V>> bucket : buckets) {  
  48.       if(bucket == nullcontinue;  
  49.       for(MapEntry<K,V> mpair : bucket)  
  50.         set.add(mpair);  
  51.     }  
  52.     return set;  
  53.   }  
  54.   public static void main(String[] args) {  
  55.     SimpleHashMap<String,String> m =  
  56.       new SimpleHashMap<String,String>();  
  57.     m.putAll(Countries.capitals(25));  
  58.     System.out.println(m);  
  59.     System.out.println(m.get("ERITREA"));  
  60.     System.out.println(m.entrySet());  
  61.   }  
  62. /* Output: 
  63. {CAMEROON=Yaounde, CONGO=Brazzaville, CHAD=N'djamena, COTE D'IVOIR (IVORY COAST)=Yamoussoukro, CENTRAL AFRICAN REPUBLIC=Bangui, GUINEA=Conakry, BOTSWANA=Gaberone, BISSAU=Bissau, EGYPT=Cairo, ANGOLA=Luanda, BURKINA FASO=Ouagadougou, ERITREA=Asmara, THE GAMBIA=Banjul, KENYA=Nairobi, GABON=Libreville, CAPE VERDE=Praia, ALGERIA=Algiers, COMOROS=Moroni, EQUATORIAL GUINEA=Malabo, BURUNDI=Bujumbura, BENIN=Porto-Novo, BULGARIA=Sofia, GHANA=Accra, DJIBOUTI=Dijibouti, ETHIOPIA=Addis Ababa} 
  64. Asmara 
  65. [CAMEROON=Yaounde, CONGO=Brazzaville, CHAD=N'djamena, COTE D'IVOIR (IVORY COAST)=Yamoussoukro, CENTRAL AFRICAN REPUBLIC=Bangui, GUINEA=Conakry, BOTSWANA=Gaberone, BISSAU=Bissau, EGYPT=Cairo, ANGOLA=Luanda, BURKINA FASO=Ouagadougou, ERITREA=Asmara, THE GAMBIA=Banjul, KENYA=Nairobi, GABON=Libreville, CAPE VERDE=Praia, ALGERIA=Algiers, COMOROS=Moroni, EQUATORIAL GUINEA=Malabo, BURUNDI=Bujumbura, BENIN=Porto-Novo, BULGARIA=Sofia, GHANA=Accra, DJIBOUTI=Dijibouti, ETHIOPIA=Addis Ababa] 
  66. *///:~  

怎樣?現在應該知道hashcode方法的作用了吧,它就是用來提高效率的,有句話說得好:爲速度而散列。因爲散列的Set和Map是基於hashcode方法來查找對象的,所以你在使用這些類的時候一定要覆蓋hashcode方法,而非散列的Set和Map,例如TreeSet,TreeMap等,它們只需equals方法就可以唯一確定對象的身份。這樣說,想必已經很清楚了吧。



第二個鏈接

1. 首先equals()和hashcode()這兩個方法都是從object類中繼承過來的。 
equals()方法在object類中定義如下: 
  public boolean equals(Object obj) { 
return (this == obj); 

很明顯是對兩個對象的地址值進行的比較(即比較引用是否相同)。但是我們必需清楚,當String 、Math、還有Integer、Double。。。。等這些封裝類在使用equals()方法時,已經覆蓋了object類的equals()方法。比如在String類中如下: 
  public boolean equals(Object anObject) { 
if (this == anObject) { 
    return true; 

if (anObject instanceof String) { 
    String anotherString = (String)anObject; 
    int n = count; 
    if (n == anotherString.count) { 
char v1[] = value; 
char v2[] = anotherString.value; 
int i = offset; 
int j = anotherString.offset; 
while (n-- != 0) { 
    if (v1[i++] != v2[j++]) 
return false; 

return true; 
    } 

return false; 

很明顯,這是進行的內容比較,而已經不再是地址的比較。依次類推Double、Integer、Math。。。。等等這些類都是重寫了equals()方法的,從而進行的是內容的比較。當然了基本類型是進行值的比較,這個沒有什麼好說的。 
我們還應該注意,Java語言對equals()的要求如下,這些要求是必須遵循的: 
• 對稱性:如果x.equals(y)返回是“true”,那麼y.equals(x)也應該返回是“true”。 
• 反射性:x.equals(x)必須返回是“true”。 
• 類推性:如果x.equals(y)返回是“true”,而且y.equals(z)返回是“true”,那麼z.equals(x)也應該返回是“true”。 
• 還有一致性:如果x.equals(y)返回是“true”,只要x和y內容一直不變,不管你重複x.equals(y)多少次,返回都是“true”。 
• 任何情況下,x.equals(null),永遠返回是“false”;x.equals(和x不同類型的對象)永遠返回是“false”。 
以上這五點是重寫equals()方法時,必須遵守的準則,如果違反會出現意想不到的結果,請大家一定要遵守。 
2. 其次是hashcode() 方法,在object類中定義如下: 
  public native int hashCode(); 
說明是一個本地方法,它的實現是根據本地機器相關的。當然我們可以在自己寫的類中覆蓋hashcode()方法,比如String、Integer、Double。。。。等等這些類都是覆蓋了hashcode()方法的。例如在String類中定義的hashcode()方法如下: 
    public int hashCode() { 
int h = hash; 
if (h == 0) { 
    int off = offset; 
    char val[] = value; 
    int len = count; 

            for (int i = 0; i < len; i++) { 
                h = 31*h + val[off++]; 
            } 
            hash = h; 
        } 
        return h; 

解釋一下這個程序(String的API中寫到): 
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] 
使用 int 算法,這裏 s[i] 是字符串的第 i 個字符,n 是字符串的長度,^ 表示求冪。(空字符串的哈希碼爲 0。) 

3.這裏我們首先要明白一個問題: 
equals()相等的兩個對象,hashcode()一定相等; 
equals()不相等的兩個對象,卻並不能證明他們的hashcode()不相等。換句話說,equals()方法不相等的兩個對象,hashcode()有可能相等。(我的理解是由於哈希碼在生成的時候產生衝突造成的)。 
反過來:hashcode()不等,一定能推出equals()也不等;hashcode()相等,equals()可能相等,也可能不等。解釋下第3點的使用範圍,我的理解是在object、String等類中都能使用。在object類中,hashcode()方法是本地方法,返回的是對象的地址值,而object類中的equals()方法比較的也是兩個對象的地址值,如果equals()相等,說明兩個對象地址值也相等,當然hashcode()也就相等了;在String類中,equals()返回的是兩個對象內容的比較,當兩個對象內容相等時, 
Hashcode()方法根據String類的重寫(第2點裏面已經分析了)代碼的分析,也可知道hashcode()返回結果也會相等。以此類推,可以知道Integer、Double等封裝類中經過重寫的equals()和hashcode()方法也同樣適合於這個原則。當然沒有經過重寫的類,在繼承了object類的equals()和hashcode()方法後,也會遵守這個原則。 

4.談到hashcode()和equals()就不能不說到hashset,hashmap,hashtable中的使用,具體是怎樣呢,請看如下分析: 
Hashset是繼承Set接口,Set接口又實現Collection接口,這是層次關係。那麼hashset是根據什麼原理來存取對象的呢? 
在hashset中不允許出現重複對象,元素的位置也是不確定的。在hashset中又是怎樣判定元素是否重複的呢?這就是問題的關鍵所在,經過一下午的查詢求證終於獲得了一點啓示,和大家分享一下,在java的集合中,判斷兩個對象是否相等的規則是: 
1),判斷兩個對象的hashCode是否相等 
      如果不相等,認爲兩個對象也不相等,完畢 
      如果相等,轉入2) 
(這一點只是爲了提高存儲效率而要求的,其實理論上沒有也可以,但如果沒有,實際使用時效率會大大降低,所以我們這裏將其做爲必需的。後面會重點講到這個問題。) 
2),判斷兩個對象用equals運算是否相等 
      如果不相等,認爲兩個對象也不相等 
      如果相等,認爲兩個對象相等(equals()是判斷兩個對象是否相等的關鍵) 
爲什麼是兩條準則,難道用第一條不行嗎?不行,因爲前面已經說了,hashcode()相等時,equals()方法也可能不等,所以必須用第2條準則進行限制,才能保證加入的爲非重複元素。 
比如下面的代碼: 

public static void main(String args[]){ 
String s1=new String("zhaoxudong"); 
String s2=new String("zhaoxudong"); 
System.out.println(s1==s2);//false 
System.out.println(s1.equals(s2));//true 
System.out.println(s1.hashCode());//s1.hashcode()等於s2.hashcode() 
System.out.println(s2.hashCode()); 
Set hashset=new HashSet(); 
hashset.add(s1); 
hashset.add(s2); 
/*實質上在添加s1,s2時,運用上面說到的兩點準則,可以知道hashset認爲s1和s2是相等的,是在添加重複元素,所以讓s2覆蓋了s1;*/ 
Iterator it=hashset.iterator(); 
            while(it.hasNext()) 
            { 
             System.out.println(it.next()); 
            } 
最後在while循環的時候只打印出了一個”zhaoxudong”。 
輸出結果爲:false 
            true 
            -967303459 
            -967303459 
這是因爲String類已經重寫了equals()方法和hashcode()方法,所以在根據上面的第1.2條原則判定時,hashset認爲它們是相等的對象,進行了重複添加。 
但是看下面的程序: 
import java.util.*; 
public class HashSetTest 

   public static void main(String[] args) 
    { 
                 HashSet hs=new HashSet(); 
                 hs.add(new Student(1,"zhangsan")); 
                 hs.add(new Student(2,"lisi")); 
                 hs.add(new Student(3,"wangwu")); 
                 hs.add(new Student(1,"zhangsan")); 
  
                 Iterator it=hs.iterator(); 
                 while(it.hasNext()) 
                 { 
                        System.out.println(it.next()); 
                 } 
     } 

class Student 
   { 
     int num; 
     String name; 
     Student(int num,String name) 
                { 
                this.num=num; 
                 this.name=name; 
                 } 
              public String toString() 
                { 
                    return num+":"+name; 
                 } 
           }      
輸出結果爲: 
                      1:zhangsan 
                   1:zhangsan 
                   3:wangwu 
                   2:lisi 
問題出現了,爲什麼hashset添加了相等的元素呢,這是不是和hashset的原則違背了呢?回答是:沒有 
因爲在根據hashcode()對兩次建立的new Student(1,"zhangsan")對象進行比較時,生成的是不同的哈希碼值,所以hashset把他當作不同的對象對待了,當然此時的equals()方法返回的值也不等(這個不用解釋了吧)。那麼爲什麼會生成不同的哈希碼值呢?上面我們在比較s1和s2的時候不是生成了同樣的哈希碼嗎?原因就在於我們自己寫的Student類並沒有重新自己的hashcode()和equals()方法,所以在比較時,是繼承的object類中的hashcode()方法,呵呵,各位還記得object類中的hashcode()方法比較的是什麼吧!! 
它是一個本地方法,比較的是對象的地址(引用地址),使用new方法創建對象,兩次生成的當然是不同的對象了(這個大家都能理解吧。。。),造成的結果就是兩個對象的hashcode()返回的值不一樣。所以根據第一個準則,hashset會把它們當作不同的對象對待,自然也用不着第二個準則進行判定了。那麼怎麼解決這個問題呢?? 
答案是:在Student類中重新hashcode()和equals()方法。 
例如: 
  class Student 

int num; 
String name; 
Student(int num,String name) 

            this.num=num; 
            this.name=name; 

public int hashCode() 

            return num*name.hashCode(); 

public boolean equals(Object o) 

            Student s=(Student)o; 
            return num==s.num && name.equals(s.name); 

public String toString() 

            return num+":"+name; 


根據重寫的方法,即便兩次調用了new Student(1,"zhangsan"),我們在獲得對象的哈希碼時,根據重寫的方法hashcode(),獲得的哈希碼肯定是一樣的(這一點應該沒有疑問吧)。 
當然根據equals()方法我們也可判斷是相同的。所以在向hashset集合中添加時把它們當作重複元素看待了。所以運行修改後的程序時,我們會發現運行結果是: 
                      1:zhangsan 
                   3:wangwu 
                   2:lisi 
可以看到重複元素的問題已經消除。 
關於在hibernate的pojo類中,重新equals()和hashcode()的問題: 
1),重點是equals,重寫hashCode只是技術要求(爲了提高效率) 
2),爲什麼要重寫equals呢,因爲在java的集合框架中,是通過equals來判斷兩個對象是否相等的 
3),在hibernate中,經常使用set集合來保存相關對象,而set集合是不允許重複的。我們再來談談前面提到在向hashset集合中添加元素時,怎樣判斷對象是否相同的準則,前面說了兩條,其實只要重寫equals()這一條也可以。 
但當hashset中元素比較多時,或者是重寫的equals()方法比較複雜時,我們只用equals()方法進行比較判斷,效率也會非常低,所以引入了hashcode()這個方法,只是爲了提高效率,但是我覺得這是非常有必要的(所以我們在前面以兩條準則來進行hashset的元素是否重複的判斷)。 
比如可以這樣寫: 
public int hashCode(){ 
   return  1;}//等價於hashcode無效 
這樣做的效果就是在比較哈希碼的時候不能進行判斷,因爲每個對象返回的哈希碼都是1,每次都必須要經過比較equals()方法後才能進行判斷是否重複,這當然會引起效率的大大降低。 
我有一個問題,如果像前面提到的在hashset中判斷元素是否重複的必要方法是equals()方法(根據網上找到的觀點),但是這裏並沒有涉及到關於哈希表的問題,可是這個集合卻叫hashset,這是爲什麼?? 
我想,在hashmap,hashtable中的存儲操作,依然遵守上面的準則。所以這裏不再多說。這些是今天看書,網上查詢資料,自己總結出來的,部分代碼和語言是引述,但是千真萬確是自己總結出來的。有錯誤之處和不詳細不清楚的地方還請大家指出,我也是初學者,所以難免會有錯誤的地方,希望大家共同討論。

發佈了7 篇原創文章 · 獲贊 6 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章