java hash map深度分析(equal,hashcode,hash)

一、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.    }  

 來自:http://kakajw.iteye.com/blog/935226

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