HashMap之Hash碰撞源碼解析


轉自:https://blog.csdn.net/luo_da/article/details/77507315

  https://www.cnblogs.com/tongxuping/p/8276198.html

HashMap是最常用的集合類框架之一,它實現了Map接口,所以存儲的元素也是鍵值對映射的結構,並允許使用null值和null鍵,其內元素是無序的,如果要保證有序,可以使用LinkedHashMap。HashMap是線程不安全的,下篇文章會討論。HashMap的類關系如下:

    java.util 

    Class HashMap<K,V>

      java.lang.Object

          |--java.util.AbstractMap<K,V>

                |--java.util.HashMap<K,V>

所有已實現的接口:

  Serializable,Cloneable,Map<K,V>

直接已知子類:

  LinkedHashMap,PrinterStateReasons

 

  HashMap中用的最多的方法就屬put() 和 get() 方法;HashMap的Key值是唯一的,那如何保證唯一性呢?我們首先想到的是用equals比較,沒錯,這樣可以實現,但隨着內部元素的增多,put和get的效率將越來越低,這里的時間復雜度是O(n),假如有1000個元素,put時最差情況需要比較1000次。實際上,HashMap很少會用到equals方法,因為其內通過一個哈希表管理所有元素,哈希是通過hash單詞音譯過來的,也可以稱為散列表,哈希算法可以快速的存取元素,當我們調用put存值時,HashMap首先會調用Key的hash方法,計算出哈希碼,通過哈希碼快速找到某個存放位置(桶),這個位置可以被稱之為bucketIndex,但可能會存在多個元素找到了相同的bucketIndex,有個專業名詞叫碰撞,當碰撞發生時,這時會取到bucketIndex位置已存儲的元素,最終通過equals來比較,equals方法就是碰撞時才會執行的方法,所以前面說HashMap很少會用到equals。HashMap通過hashCode和equals最終判斷出Key是否已存在,如果已存在,則使用新Value值替換舊Value值,並返回舊Value值,如果不存在 ,則存放新的鍵值對<K, V>到bucketIndex位置。通過下面的流程圖來梳理一下整個put過程。

           

 

 

最終HashMap的存儲結構會有這三種情況,我們當然期望情形3是最少發生的(效率最低)。

 

HashMap 碰撞問題處理:

  碰撞:所謂“碰撞”就上面所述是多個元素計算得出相同的hashCode,在put時出現沖突。

  處理方法:

  Java中HashMap是利用“拉鏈法”處理HashCode的碰撞問題。在調用HashMap的put方法或get方法時,都會首先調用hashcode方法,去查找相關的key,當有沖突時,再調用equals方法。hashMap基於hasing原理,我們通過put和get方法存取對象。當我們將鍵值對傳遞給put方法時,他調用鍵對象的hashCode()方法來計算hashCode,然后找到bucket(哈希桶)位置來存儲對象。當獲取對象時,通過鍵對象的equals()方法找到正確的鍵值對,然后返回值對象。HashMap使用鏈表來解決碰撞問題,當碰撞發生了,對象將會存儲在鏈表的下一個節點中。hashMap在每個鏈表節點存儲鍵值對對象。當兩個不同的鍵卻有相同的hashCode時,他們會存儲在同一個bucket位置的鏈表中。鍵對象的equals()來找到鍵值對。

 HashMap基本結構概念圖:

      

 

到目前為止,我們了解了兩件事:

  1、HashMap通過鍵的hashCode來快速的存取元素。

  2、當不同的對象發生碰撞時,HashMap通過單鏈表來解決,將新元素加入鏈表表頭,通過next指向原有的元素。單鏈表在Java中的實現就是對象的引用(復合)。

HashMap.put()和get()源碼:

 

 1 /** 
 2  * Returns the value to which the specified key is mapped, 
 3  * or if this map contains no mapping for the key. 
 4  * 
 5  * 獲取key對應的value 
 6  */  
 7 public V get(Object key) {  
 8     if (key == null)  
 9         return getForNullKey();  
10     //獲取key的hash值  
11     int hash = hash(key.hashCode());  
12     // 在“該hash值對應的鏈表”上查找“鍵值等於key”的元素  
13     for (Entry<K,V> e = table[indexFor(hash, table.length)];  
14          e != null;  
15          e = e.next) {  
16         Object k;  
17         if (e.hash == hash && ((k = e.key) == key || key.equals(k)))  
18             return e.value;  
19     }  
20     return null;  
21 }  
22 
23 /** 
24  * Offloaded version of get() to look up null keys.  Null keys map 
25  * to index 0.   
26  * 獲取key為null的鍵值對,HashMap將此鍵值對存儲到table[0]的位置 
27  */  
28 private V getForNullKey() {  
29     for (Entry<K,V> e = table[0]; e != null; e = e.next) {  
30         if (e.key == null)  
31             return e.value;  
32     }  
33     return null;  
34 }  
35 
36 /** 
37  * Returns <tt>true</tt> if this map contains a mapping for the 
38  * specified key. 
39  * 
40  * HashMap是否包含key 
41  */  
42 public boolean containsKey(Object key) {  
43     return getEntry(key) != null;  
44 }  
45 
46 /** 
47  * Returns the entry associated with the specified key in the 
48  * HashMap.   
49  * 返回鍵為key的鍵值對 
50  */  
51 final Entry<K,V> getEntry(Object key) {  
52     //先獲取哈希值。如果key為null,hash = 0;這是因為key為null的鍵值對存儲在table[0]的位置。  
53     int hash = (key == null) ? 0 : hash(key.hashCode());  
54     //在該哈希值對應的鏈表上查找鍵值與key相等的元素。  
55     for (Entry<K,V> e = table[indexFor(hash, table.length)];  
56          e != null;  
57          e = e.next) {  
58         Object k;  
59         if (e.hash == hash &&  
60             ((k = e.key) == key || (key != null && key.equals(k))))  
61             return e;  
62     }  
63     return null;  
64 }  
65 
66 
67 /** 
68  * Associates the specified value with the specified key in this map. 
69  * If the map previously contained a mapping for the key, the old 
70  * value is replaced. 
71  * 
72  * 將“key-value”添加到HashMap中,如果hashMap中包含了key,那么原來的值將會被新值取代 
73  */  
74 public V put(K key, V value) {  
75     //如果key是null,那么調用putForNullKey(),將該鍵值對添加到table[0]中  
76     if (key == null)  
77         return putForNullKey(value);  
78     //如果key不為null,則計算key的哈希值,然后將其添加到哈希值對應的鏈表中  
79     int hash = hash(key.hashCode());  
80     int i = indexFor(hash, table.length);  
81     for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
82         Object k;  
83         //如果這個key對應的鍵值對已經存在,就用新的value代替老的value。  
84         if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
85             V oldValue = e.value;  
86             e.value = value;  
87             e.recordAccess(this);  
88             return oldValue;  
89         }  
90     }  
91 
92     modCount++;  
93     addEntry(hash, key, value, i);  
94     return null;  
95 }

 

從HashMap的put()和get方法實現中可以與拉鏈法解決hashCode沖突解決方法相互印證。並且從put方法中可以看出HashMap是使用Entry<K,V>來存儲數據。

Java8碰撞優化提升

   為什么會有這么大的性能提升,盡管這里用的是大O符號(大O描述的是漸近上界)?其實這個優化在JEP-180中已經提到了。如果某個桶中的記錄過大的話(當前是TREEIFY_THRESHOLD = 8),HashMap會動態的使用一個專門的treemap實現來替換掉它。這樣做的結果會更好,是O(logn),而不是糟糕的O(n)。它是如何工作的?前面產生沖突的那些KEY對應的記錄只是簡單的追加到一個鏈表后面,這些記錄只能通過遍歷來進行查找。但是超過這個閾值后HashMap開始將列表升級成一個二叉樹,使用哈希值作為樹的分支變量,如果兩個哈希值不等,但指向同一個桶的話,較大的那個會插入到右子樹里。如果哈希值相等,HashMap希望key值最好是實現了Comparable接口的,這樣它可以按照順序來進行插入。這對HashMap的key來說並不是必須的,不過如果實現了當然最好。如果沒有實現這個接口,在出現嚴重的哈希碰撞的時候,你就並別指望能獲得性能提升了。這個性能提升有什么用處?比方說惡意的程序,如果它知道我們用的是哈希算法,它可能會發送大量的請求,導致產生嚴重的哈希碰撞。然后不停的訪問這些key就能顯著的影響服務器的性能,這樣就形成了一次拒絕服務攻擊(DoS)。JDK 8中從O(n)到O(logn)的飛躍,可以有效地防止類似的攻擊,同時也讓HashMap性能的可預測性稍微增強了一些。

 


免責聲明!

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



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