深入解析hashcode,hashMap源碼


hashCode 的常規協定是: 

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

以上這段官方文檔的定義,我們可以抽出成以下幾個關鍵點:

1、hashCode的存在主要是用於查找的快捷性,如Hashtable,HashMap等,hashCode是用來在散列存儲結構中確定對象的存儲地址的;

2、如果兩個對象相同,就是適用於equals(java.lang.Object) 方法,那麼這兩個對象的hashCode一定要相同;

3、如果對象的equals方法被重寫,那麼對象的hashCode也儘量重寫,並且產生hashCode使用的對象,一定要和equals方法中使用的一致,否則就會違反上面提到的第2點;

4、兩個對象的hashCode相同,並不一定表示兩個對象就相同,也就是不一定適用於equals(java.lang.Object) 方法,只能夠說明這兩個對象在散列存儲結構中,如Hashtable,他們“存放在同一個籃子裏”


再歸納一下就是hashCode是用於查找使用的,而equals是用於比較兩個對象的是否相等的。以下這段話是從別人帖子回覆拷貝過來的:

1.hashcode是用來查找的,如果你學過數據結構就應該知道,在查找和排序這一章有
例如內存中有這樣的位置
0  1  2  3  4  5  6  7  
而我有個類,這個類有個字段叫ID,我要把這個類存放在以上8個位置之一,如果不用hashcode而任意存放,那麼當查找時就需要到這八個位置裏挨個去找,或者用二分法一類的算法。
但如果用hashcode那就會使效率提高很多。
我們這個類中有個字段叫ID,那麼我們就定義我們的hashcode爲ID%8,然後把我們的類存放在取得得餘數那個位置。比如我們的ID爲9,9除8的餘數爲1,那麼我們就把該類存在1這個位置,如果ID是13,求得的餘數是5,那麼我們就把該類放在5這個位置。這樣,以後在查找該類時就可以通過ID除 8求餘數直接找到存放的位置了。
2.但是如果兩個類有相同的hashcode怎麼辦那(我們假設上面的類的ID不是唯一的),例如9除以8和17除以8的餘數都是1,那麼這是不是合法的,回答是:可以這樣。那麼如何判斷呢?在這個時候就需要定義 equals了。
也就是說,我們先通過 hashcode來判斷兩個類是否存放某個桶裏,但這個桶裏可能有很多類,那麼我們就需要再通過 equals 來在這個桶裏找到我們要的類。
那麼。重寫了equals(),爲什麼還要重寫hashCode()呢?
想想,你要在一個桶裏找東西,你必須先要找到這個桶啊,你不通過重寫hashcode()來找到桶,光重寫equals()有什麼用啊

一、java對象的比較

等號(==):

對比對象實例的內存地址(也即對象實例的ID),來判斷是否是同一對象實例;又可以說是判斷對象實例是否物理相等; 

equals():

對比兩個對象實例是否相等。

當對象所屬的類沒有重寫根類Object的equals()方法時,equals()判斷的是對象實例的ID(內存地址),是否是同一對象實例;該方法就是使用的等號(==)的判斷結果,如Object類的源代碼所示:

Java代碼  收藏代碼

  1. public boolean equals(Object obj) {  
  2.       return (this == obj);  
  3. }  

 
當對象所屬的類重寫equals()方法(可能因爲需要自己特有的“邏輯相等”概念)時,equals()判斷的根據就因具體實現而異,有些類是需要比較對象的某些指或內容,如String類重寫equals()來判斷字符串的值是否相等。判斷邏輯相等。

hashCode():

計算出對象實例的哈希碼,並返回哈希碼,又稱爲散列函數。根類Object的hashCode()方法的計算依賴於對象實例的D(內存地址),故每個Object對象的hashCode都是唯一的;當然,當對象所對應的類重寫了hashCode()方法時,結果就截然不同了。


二、Java的類爲什麼需要hashCode?---hashCode的作用,從Java中的集合的角度看。

  總的來說,Java中的集合(Collection)有兩類,一類是List,再有一類是Set。你知道它們的區別嗎?前者集合內的元素是有序的,元素可以重複;後者元素無序,但元素不可重複。那麼這裏就有一個比較嚴重的問題了:要想保證元素不重複,可兩個元素是否重複應該依據什麼來判斷呢?這就是 Object.equals方法了。但是,如果每增加一個元素就檢查一次,那麼當元素很多時,後添加到集合中的元素比較的次數就非常多了。也就是說,如果集合中現在已經有1000個元素,那麼第1001個元素加入集合時,它就要調用1000次equals方法。這顯然會大大降低效率。   

    於是,Java採用了哈希表的原理。哈希算法也稱爲散列算法,是將數據依特定算法直接指定到一個地址上。關於哈希算法,這裏就不詳細介紹。可以這樣簡單理解,hashCode方法實際上返回的就是對象存儲位置的映像。   

     這樣一來,當集合要添加新的元素時,先調用這個元素的hashCode方法,就能定位到它應該放置的存儲位置。如果這個位置上沒有元素,它就可以直接存儲在這個位置上,不用再進行任何比較了;如果這個位置上已經有元素了,就調用它的equals方法與新元素進行比較,相同的話就不存了,不相同就表示發生衝突了,散列表對於衝突有具體的解決辦法,但最終還會將新元素保存在適當的位置。這樣一來,實際調用equals方法的次數就大大降低了,幾乎只需要一兩次。   

所以,Java對於eqauls方法和hashCode方法是這樣規定的:
1、相等的對象必須具有相等的哈希碼(或者散列碼)。
2、如果兩個對象的hashCode相同,它們並不一定相同。
   
 上述的對象相同指的是通過eqauls方法判斷,結果爲true。

 你當然可以不按要求去做了,但你會發現,相同的對象可以出現在Set集合中。同時,增加新元素的效率會大大下降。 

三、深入解析HashMap類的底層數據結構

Map接口
  Map沒有繼承Collection接口,Map提供key到value的映射。一個Map中不能包含相同的key,每個key只能映射一個 value。Map接口提供3種集合的視圖,Map的內容可以被當作一組key集合,一組value集合,或者一組key-value映射。 

Hashtable類
  Hashtable繼承Map接口,實現一個key-value映射的哈希表。任何非空(non-null)的對象都可作爲key或者value。添加數據使用put(key, value),取出數據使用get(key),這兩個基本操作的時間開銷爲常數。   

    Hashtable通過initial capacity和load factor兩個參數調整性能。通常缺省的load factor 0.75較好地實現了時間和空間的均衡。增大load factor可以節省空間但相應的查找時間將增大,這會影響像get和put這樣的操作。
使用Hashtable的簡單示例如下,將1,2,3放到Hashtable中,他們的key分別是”one”,”two”,”three”:
    Hashtable numbers = new Hashtable();
    numbers.put(“one”, new Integer(1));
    numbers.put(“two”, new Integer(2));
    numbers.put(“three”, new Integer(3));
  要取出一個數,比如2,用相應的key:
    Integer n = (Integer)numbers.get(“two”);
    System.out.println(“two = ” + n);

 
1.    HashMap概述:
   HashMap是基於哈希表的Map接口的非同步實現。此實現提供所有可選的映射操作,並允許使用null值和null鍵。此類不保證映射的順序,特別是它不保證該順序恆久不變。
 
2.    HashMap的數據結構:
    HashMap實際上是一個“鏈表散列”的數據結構,
即數組和鏈表的結合體。首先,HashMap類的屬性中定義了Entry類型的數組。Entry類實現java.ultil.Map.Entry接口,同時每一對key和value是作爲Entry類的屬性被包裝在Entry的類中。 

如圖所示,HashMap的數據結構:


 

 

 

   HashMap的部分源碼如下:

Java代碼  收藏代碼
  1. /** 
  2.  * The table, resized as necessary. Length MUST Always be a power of two. 
  3.  */  
  4.   
  5. transient Entry[] table;  
  6.    
  7. static class Entry<K,V> implements Map.Entry<K,V> {  
  8.     final K key;  
  9.     V value;  
  10.     Entry<K,V> next;  
  11.     final int hash;  
  12.     ……  
  13. }  

 
    可以看出,HashMap底層就是一個數組結構,數組中的每一項又是一個鏈表。當新建一個HashMap的時候,就會初始化一個數組。table數組的元素是Entry類型的。每個 Entry元素其實就是一個key-value對,並且它持有一個指向下一個 Entry元素的引用,這就說明table數組的每個Entry元素同時也作爲某個Entry鏈表的首節點,指向了該鏈表的下一個Entry元素,這就是所謂的“鏈表散列”數據結構,即數組和鏈表的結合體。 

3.    HashMap的存取實現:

   1) 添加元素:

當我們往HashMap中put元素的時候,先根據key的重新計算元素的hashCode,根據hashCode得到這個元素在table數組中的位置(即下標),如果數組該位置上已經存放有其他元素了,那麼在這個位置上的元素將以鏈表的形式存放,新加入的放在鏈頭,最先加入的放在鏈尾。如果數組該位置上沒有元素,就直接將該元素放到此數組中的該位置上。

HashMap的部分源碼如下:

Java代碼  收藏代碼
  1. public V put(K key, V value) {  
  2.    // HashMap允許存放null鍵和null值。  
  3.    // 當key爲null時,調用putForNullKey方法,將value放置在數組第一個位置。  
  4.    if (key == null)  
  5.        return putForNullKey(value);  
  6.    // 根據key的keyCode重新計算hash值。  
  7.    int hash = hash(key.hashCode());  
  8.    // 搜索指定hash值在對應table中的索引。  
  9.    int i = indexFor(hash, table.length);  
  10.    // 如果 i 索引處的 Entry 不爲 null,通過循環不斷遍歷 e 元素的下一個元素。  
  11.    for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
  12.        Object k;  
  13.       // 如果發現 i 索引處的鏈表的某個Entry的hash和新Entry的hash相等且兩者的key相同,則新Entry覆蓋舊Entry,返回。  
  14.        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
  15.            V oldValue = e.value;  
  16.            e.value = value;  
  17.            e.recordAccess(this);  
  18.            return oldValue;  
  19.        }  
  20.    }  
  21.    // 如果i索引處的Entry爲null,表明此處還沒有Entry。  
  22.    modCount++;  
  23.    // 將key、value添加到i索引處。  
  24.    addEntry(hash, key, value, i);  
  25.    return null;  

 

 2) 讀取元素:

有了上面存儲時的hash算法作爲基礎,理解起來這段代碼就很容易了。從上面的源代碼中可以看出:從HashMap中get元素時,首先計算key的hashCode,找到數組中對應位置的某一元素,然後通過key的equals方法在對應位置的鏈表中找到需要的元素。

HashMap的部分源碼如下:

Java代碼  收藏代碼
  1. public V get(Object key) {  
  2.     if (key == null)  
  3.         return getForNullKey();  
  4.     int hash = hash(key.hashCode());  
  5.     for (Entry<K,V> e = table[indexFor(hash, table.length)];  
  6.         e != null;  
  7.         e = e.next) {  
  8.         Object k;  
  9.         if (e.hash == hash && ((k = e.key) == key || key.equals(k)))  
  10.             return e.value;  
  11.     }  
  12.     return null;  
  13. }   

  
   3) 歸納起來簡單地說,HashMap 在底層將 key-value 當成一個整體進行處理,這個整體就是一個 Entry 對象。HashMap 底層採用一個 Entry[] 數組來保存所有的 key-value 對,當需要存儲一個 Entry 對象時,會根據hash算法來決定其在數組中的存儲位置,在根據equals方法決定其在該數組位置上的鏈表中的存儲位置;當需要取出一個Entry時,也會根據hash算法找到其在數組中的存儲位置,再根據equals方法從該位置上的鏈表中取出該Entry。   

四、關於“相等的對象必須具有相等的哈希碼”

  由於作爲key的對象將通過計算其散列函數hashCode()來確定與之對應的value的位置,因此任何作爲key的對象都必須實現hashCode和equals方法。hashCode和equals方法繼承自根類Object,如果你用自定義的類當作key的話,要相當小心,按照散列函數的定義,如果兩個對象相同,即obj1.equals(obj2)=true,則它們的hashCode必須相同,但如果兩個對象不同,則它們的hashCode不一定不同,如果兩個不同對象的hashCode相同,這種現象稱爲衝突,衝突會導致操作哈希表的時間開銷增大,所以儘量定義好的hashCode()方法,能加快哈希表的操作。
  如果相同的對象有不同的hashCode,對哈希表的操作會出現意想不到的結果(期待的get方法返回null),要避免這種問題,只需要牢記一條:要同時複寫equals方法和hashCode方法,而不要只寫其中一個。

同時複寫equals方法和hashCode方法,必須保證“相等的對象必須具有相等的哈希碼”,也就是當兩個對象通過equals()比較的結果爲true時,這兩個對象調用hashCode()方法生成的哈希碼必須相等。 

如何保證相等,可以參考下面的方法:

    複寫equals方法和hashCode方法時,equals方法的判斷根據和計算hashCode的依據相同。如String的equals方法是比較字符串每個字符,String的hashCode也是通過對該字符串每個字符的ASC碼簡單的算術運算所得,這樣就可以保證相同的字符串的hashCode相同且equals()爲真。 

String類的equals方法的源代碼:

Java代碼  收藏代碼
  1.    /** 
  2.     * Compares this string to the specified object.  The result is {@code 
  3.     * true} if and only if the argument is not {@code null} and is a {@code 
  4.     * String} object that represents the same sequence of characters as this 
  5.     * object. 
  6.     * 
  7.     * @param  anObject 
  8.     *         The object to compare this {@code String} against 
  9.     * 
  10.     * @return  {@code true} if the given object represents a {@code String} 
  11.     *          equivalent to this string, {@code false} otherwise 
  12.     * 
  13.     * @see  #compareTo(String) 
  14.     * @see  #equalsIgnoreCase(String) 
  15.     */  
  16.    public boolean equals(Object anObject) {  
  17. if (this == anObject) {  
  18.     return true;  
  19. }  
  20. if (anObject instanceof String) {  
  21.     String anotherString = (String)anObject;  
  22.     int n = count;  
  23.     if (n == anotherString.count) {  
  24.     char v1[] = value;  
  25.     char v2[] = anotherString.value;  
  26.     int i = offset;  
  27.     int j = anotherString.offset;  
  28.     while (n-- != 0) {  
  29.         if (v1[i++] != v2[j++])  
  30.         return false;  
  31.     }  
  32.     return true;  
  33.     }  
  34. }  
  35. return false;  
  36.    }  

 

String類的hashCode方法計算hashCode的源代碼:

Java代碼  收藏代碼
  1.    /** 
  2.     * Returns a hash code for this string. The hash code for a 
  3.     * <code>String</code> object is computed as 
  4.     * <blockquote><pre> 
  5.     * s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] 
  6.     * </pre></blockquote> 
  7.     * using <code>int</code> arithmetic, where <code>s[i]</code> is the 
  8.     * <i>i</i>th character of the string, <code>n</code> is the length of 
  9.     * the string, and <code>^</code> indicates exponentiation. 
  10.     * (The hash value of the empty string is zero.) 
  11.     * 
  12.     * @return  a hash code value for this object. 
  13.     */  
  14.    public int hashCode() {  
  15. int h = hash;  
  16.        int len = count;  
  17. if (h == 0 && len > 0) {  
  18.     int off = offset;  
  19.     char val[] = value;  
  20.   
  21.            for (int i = 0; i < len; i++) {  
  22.                h = 31*h + val[off++];  
  23.            }  
  24.            hash = h;  
  25.        }  
  26.        return h;  
  27.    }  

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章