HashMap之put方法流程解讀


說明:本文中所談論的HashMap基於JDK 1.8版本源碼進行分析和說明。

HashMap的put方法算是HashMap中比較核心的功能了,復雜程度高但是算法巧妙,同時在上一版本的基礎之上優化了存儲結構,從鏈表逐步進化成了紅黑樹,以滿足存取性能上的需要。本文逐行分析了put方法的執行流程,重點放在了對整個流程的把握,對於紅黑樹的執行邏輯只是點到為止,其實HashMap中還有很多細節算法值得分析和學習,本文沒有涉及,算是拋磚引玉吧,后面抽空把其他的地方分析一番。

源碼閱讀與分析

1、HashMap的put方法,翻看源碼:

 1 /**
 2  * Associates the specified value with the specified key in this map.
 3  * If the map previously contained a mapping for the key, the old
 4  * value is replaced.
 5  *
 6  * @param key key with which the specified value is to be associated
 7  * @param value value to be associated with the specified key
 8  * @return the previous value associated with <tt>key</tt>, or
 9  *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
10  *         (A <tt>null</tt> return can also indicate that the map
11  *         previously associated <tt>null</tt> with <tt>key</tt>.)
12  */
13 public V put(K key, V value) {
14     return putVal(hash(key), key, value, false, true);
15 }

2、緊接着調用內部方法putVal:

 1 /**
 2  * Implements Map.put and related methods
 3  *
 4  * @param hash hash for key
 5  * @param key the key
 6  * @param value the value to put
 7  * @param onlyIfAbsent if true, don't change existing value
 8  * @param evict if false, the table is in creation mode.
 9  * @return previous value, or null if none
10  */
11 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
12                boolean evict) {
13     Node<K,V>[] tab; Node<K,V> p; int n, i;
14     if ((tab = table) == null || (n = tab.length) == 0)
15         n = (tab = resize()).length;
16     if ((p = tab[i = (n - 1) & hash]) == null)
17         tab[i] = newNode(hash, key, value, null);
18     else {
19         Node<K,V> e; K k;
20         if (p.hash == hash &&
21             ((k = p.key) == key || (key != null && key.equals(k))))
22             e = p;
23         else if (p instanceof TreeNode)
24             e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
25         else {
26             for (int binCount = 0; ; ++binCount) {
27                 if ((e = p.next) == null) {
28                     p.next = newNode(hash, key, value, null);
29                     if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
30                         treeifyBin(tab, hash);
31                     break;
32                 }
33                 if (e.hash == hash &&
34                     ((k = e.key) == key || (key != null && key.equals(k))))
35                     break;
36                 p = e;
37             }
38         }
39         if (e != null) { // existing mapping for key
40             V oldValue = e.value;
41             if (!onlyIfAbsent || oldValue == null)
42                 e.value = value;
43             afterNodeAccess(e);
44             return oldValue;
45         }
46     }
47     ++modCount;
48     if (++size > threshold)
49         resize();
50     afterNodeInsertion(evict);
51     return null;
52 }

拆解源碼進行解析

1、put方法的注釋

我們先把源碼中的注釋大致翻譯一遍:

1 Associates the specified value with the specified key in this map.
2 If the map previously contained a mapping for the key, the old
3 value is replaced.
4 @param key key with which the specified value is to be associated
5 @param value value to be associated with the specified key
6 @return the previous value associated with <tt>key</tt>, or
7         <tt>null</tt> if there was no mapping for <tt>key</tt>.
8         (A <tt>null</tt> return can also indicate that the map
9         previously associated <tt>null</tt> with <tt>key</tt>.)

大意為:將指定的值與此映射中的指定鍵相關聯,如果Map中已經包含了該鍵的映射,那么舊的映射值將會被替代,也就是說在put時,如果map中已經包含有key所關聯的鍵值對,那么后續put進來的鍵值對,將會以相同key為准替換掉原來的那一對鍵值對。

返回的值則將是之前在map中實際與key相關聯的Value值(也就是舊的值),如果key沒有實際映射值的話那就返回null。

put方法作為對外暴露的方法,在內部實現時則立馬調用了其內部putVal方法,並將put進去(覆蓋)之前的結果k-v中的v進行了返回,但map中最新綁定的那一對k-v中的v已經是最新put的了。

1 /**
2 * 對外暴露的put方法
3 **/
4 public V put(K key, V value) {
5     return putVal(hash(key), key, value, false, true);
6 }

2、putVal方法中的第一個參數hash

我們先把源碼中的方法上面的注釋先瀏覽一遍:

 1 Computes key.hashCode() and spreads (XORs) higher bits of hash
 2 to lower.  Because the table uses power-of-two masking, sets of
 3 hashes that vary only in bits above the current mask will
 4 always collide. (Among known examples are sets of Float keys
 5 holding consecutive whole numbers in small tables.)  So we
 6 apply a transform that spreads the impact of higher bits
 7 downward. There is a tradeoff between speed, utility, and
 8 quality of bit-spreading. Because many common sets of hashes
 9 are already reasonably distributed (so don't benefit from
10 spreading), and because we use trees to handle large sets of
11 collisions in bins, we just XOR some shifted bits in the
12 cheapest possible way to reduce systematic lossage, as well as
13 to incorporate impact of the highest bits that would otherwise
14 never be used in index calculations because of table bounds.

大意為:將key的hashcode值(由native方法計算得到)再與該值的高16位進行異或運算得到最終的hash值。這樣做的目的作者也給出了解釋,就是通常的hash算法都總是碰撞,我們這樣做的目的盡量使得hash值較為分散。(大概理解)

3、開始解析putVal里面的方法

 1 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
 2                boolean evict) {
 3     Node<K,V>[] tab; Node<K,V> p; int n, i;
 4     // 如果map還是空的,則先開始初始化,table是map中用於存放索引的表
 5     if ((tab = table) == null || (n = tab.length) == 0) {
 6         n = (tab = resize()).length;
 7     }
 8     // 使用hash與數組長度減一的值進行異或得到分散的數組下標,預示着按照計算現在的
 9     // key會存放到這個位置上,如果這個位置上沒有值,那么直接新建k-v節點存放
10     // 其中長度n是一個2的冪次數
11     if ((p = tab[i = (n - 1) & hash]) == null) {
12         tab[i] = newNode(hash, key, value, null);
13     } 
14     // 如果走到else這一步,說明key索引到的數組位置上已經存在內容,即出現了碰撞
15     // 這個時候需要更為復雜處理碰撞的方式來處理,如鏈表和樹
16     else {
17         Node<K,V> e; K k;
18         // 其中p已經在上面通過計算索引找到了,即發生碰撞那一個節點
19         // 比較,如果該節點的hash和當前的hash相等,而且key也相等或者
20         // 在key不等於null的情況下key的內容也相等,則說明兩個key是
21         // 一樣的,則將當前節點p用臨時節點e保存
22         if (p.hash == hash &&
23             ((k = p.key) == key || (key != null && key.equals(k)))) {
24             e = p;
25         }
26         // 如果當前節點p是(紅黑)樹類型的節點,則需要特殊處理
27         // 如果是樹,則說明碰撞已經開始用樹來處理,后續的數據結構都是樹而非
28         // 列表了
29         else if (p instanceof TreeNode) {
30             // 其中this表示當前HashMap, tab為map中的數組
31             e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
32         }
33         else {
34             for (int binCount = 0; ; ++binCount) {
35                 // 如果當前碰撞到的節點沒有后續節點,則直接新建節點並追加
36                 if ((e = p.next) == null) {
37                     p.next = newNode(hash, key, value, null);
38                     // TREEIFY_THRESHOLD = 8
39                     // 從0開始的,如果到了7則說明滿8了,這個時候就需要轉
40                     // 重新確定是否是擴容還是轉用紅黑樹了
41                     if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
42                         treeifyBin(tab, hash);
43                     break;
44                 }
45                 // 找到了碰撞節點中,key完全相等的節點,則用新節點替換老節點
46                 if (e.hash == hash &&
47                     ((k = e.key) == key || (key != null && key.equals(k))))
48                     break;
49                 p = e;
50             }
51         }
52         // 此時的e是保存的被碰撞的那個節點,即老節點
53         if (e != null) { // existing mapping for key
54             V oldValue = e.value;
55             // onlyIfAbsent是方法的調用參數,表示是否替換已存在的值,
56             // 在默認的put方法中這個值是false,所以這里會用新值替換舊值
57             if (!onlyIfAbsent || oldValue == null)
58                 e.value = value;
59             // Callbacks to allow LinkedHashMap post-actions
60             afterNodeAccess(e);
61             return oldValue;
62         }
63     }
64     // map變更性操作計數器
65     // 比如map結構化的變更像內容增減或者rehash,這將直接導致外部map的並發
66     // 迭代引起fail-fast問題,該值就是比較的基礎
67     ++modCount;
68     // size即map中包括k-v數量的多少
69     // 當map中的內容大小已經觸及到擴容閾值時,則需要擴容了
70     if (++size > threshold)
71         resize();
72     // Callbacks to allow LinkedHashMap post-actions
73     afterNodeInsertion(evict);
74     return null;
75 }

4、當存儲值發生碰撞,解決的方法已經轉換為紅黑樹時,先看下紅黑樹的數據結構:

 1 /**
 2  * Entry for Tree bins. Extends LinkedHashMap.Entry (which in turn
 3  * extends Node) so can be used as extension of either regular or
 4  * linked node.
 5  */
 6 static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
 7     TreeNode<K,V> parent;  // red-black tree links
 8     TreeNode<K,V> left;
 9     TreeNode<K,V> right;
10     TreeNode<K,V> prev;    // needed to unlink next upon deletion
11     boolean red;
12 }

5、當存儲值發生碰撞,並在當前節點已經延申到樹時,將執行putTreeVal方法:

 1 /**
 2  * Tree version of putVal.
 3  */
 4 final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
 5                                int h, K k, V v) {
 6     Class<?> kc = null;
 7     boolean searched = false;
 8     TreeNode<K,V> root = (parent != null) ? root() : this;
 9     for (TreeNode<K,V> p = root;;) {
10         int dir, ph; K pk;
11         if ((ph = p.hash) > h)
12             dir = -1;
13         else if (ph < h)
14             dir = 1;
15         else if ((pk = p.key) == k || (k != null && k.equals(pk)))
16             return p;
17         else if ((kc == null &&
18                   (kc = comparableClassFor(k)) == null) ||
19                  (dir = compareComparables(kc, k, pk)) == 0) {
20             if (!searched) {
21                 TreeNode<K,V> q, ch;
22                 searched = true;
23                 if (((ch = p.left) != null &&
24                      (q = ch.find(h, k, kc)) != null) ||
25                     ((ch = p.right) != null &&
26                      (q = ch.find(h, k, kc)) != null))
27                     return q;
28             }
29             dir = tieBreakOrder(k, pk);
30         }
31 
32         TreeNode<K,V> xp = p;
33         if ((p = (dir <= 0) ? p.left : p.right) == null) {
34             Node<K,V> xpn = xp.next;
35             TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
36             if (dir <= 0)
37                 xp.left = x;
38             else
39                 xp.right = x;
40             xp.next = x;
41             x.parent = x.prev = xp;
42             if (xpn != null)
43                 ((TreeNode<K,V>)xpn).prev = x;
44             moveRootToFront(tab, balanceInsertion(root, x));
45             return null;
46         }
47     }
48 }

這里面大概是一個紅黑樹的儲值計算方法,需要有數據結構的理論知識加持,初看有點晦澀難懂。

6、在值發生碰撞並需要延續追加時,如果追加的鏈表長度大於8,那么需要重新評估當前是擴充數組還是將鏈表轉換為紅黑樹來存儲

 1 /**
 2  * Replaces all linked nodes in bin at index for given hash unless
 3  * table is too small, in which case resizes instead.
 4  */
 5 final void treeifyBin(Node<K,V>[] tab, int hash) {
 6     int n, index; Node<K,V> e;
 7     // MIN_TREEIFY_CAPACITY = 64
 8     // 如果當前map的數組為空,或者數組長度還小於64,則選擇擴充數組長度
 9     if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) {
10         // 擴充數組長度涉及到原內容的重新散列再存儲
11         resize();
12     }
13     // 如果執行else if則說明數組長度已經大於64了,這個時候就使用了
14     // 紅黑樹來處理
15     else if ((e = tab[index = (n - 1) & hash]) != null) {
16         TreeNode<K,V> hd = null, tl = null;
17         do {
18             TreeNode<K,V> p = replacementTreeNode(e, null);
19             if (tl == null)
20                 hd = p;
21             else {
22                 p.prev = tl;
23                 tl.next = p;
24             }
25             tl = p;
26         } while ((e = e.next) != null);
27         if ((tab[index] = hd) != null)
28             // table表從此節點鏈接成樹
29             hd.treeify(tab);
30     }
31 }

7、擴充數組長度方法resize,會將整個map中的k-v對重新散列存儲,會消耗性能

  1 /**
  2  * Initializes or doubles table size.  If null, allocates in
  3  * accord with initial capacity target held in field threshold.
  4  * Otherwise, because we are using power-of-two expansion, the
  5  * elements from each bin must either stay at same index, or move
  6  * with a power of two offset in the new table.
  7  *
  8  * @return the table
  9  */
 10 final Node<K,V>[] resize() {
 11     Node<K,V>[] oldTab = table;
 12     int oldCap = (oldTab == null) ? 0 : oldTab.length;
 13     int oldThr = threshold;
 14     int newCap, newThr = 0;
 15     if (oldCap > 0) {
 16         // MAXIMUM_CAPACITY = 1 << 30 = 1073741824
 17         // Integer.MAX_VALUE = (1 << 31) - 1 = 2147483647
 18         // 如果已經到了最大容量了,那么就調整擴容的threshold閾值
 19         if (oldCap >= MAXIMUM_CAPACITY) {
 20             threshold = Integer.MAX_VALUE;
 21             return oldTab;
 22         }
 23         // DEFAULT_INITIAL_CAPACITY = 1 << 4
 24         // 否則的話,如果將目前的容量擴充2倍還在允許范圍之內,則將容量
 25         // 擴充為原來的兩倍,並且閾值也為原來的兩倍
 26         else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
 27                  oldCap >= DEFAULT_INITIAL_CAPACITY)
 28             newThr = oldThr << 1; // double threshold
 29     }
 30     // 如果原始(或者初始)容量不大於0,且之前的閾值大於0,則將容量初始化為
 31     // 之前閾值的大小
 32     else if (oldThr > 0) // initial capacity was placed in threshold
 33         newCap = oldThr;
 34     else {               // zero initial threshold signifies using defaults
 35         // 執行這里的方法說明,初始參數中容量大小和閾值都不大於0,那么就用
 36         // map中的缺省值
 37         // DEFAULT_INITIAL_CAPACITY = 1 << 4 = 16
 38         // DEFAULT_LOAD_FACTOR = 0.75f
 39         newCap = DEFAULT_INITIAL_CAPACITY;
 40         newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
 41     }
 42     // 如果新的閾值沒有重新計算,那么先用加載因子計算出值
 43     // 如果新的容量大小和閾值大小都未超過限定值,則計算出的值可用,否則
 44     // 閾值就限定為容量真正允許的上限即Integer.MAX_VALUE
 45     if (newThr == 0) {
 46         float ft = (float)newCap * loadFactor;
 47         newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
 48                   (int)ft : Integer.MAX_VALUE);
 49     }
 50     threshold = newThr;
 51     @SuppressWarnings({"rawtypes","unchecked"})
 52         Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
 53     // table已經是擴容好的新table了
 54     // 老的table存在了oldTab中
 55     table = newTab;
 56     // 以下就是一個重新散列存儲的過程了
 57     // 將老的tab中的node,按照key重新散列得到新得存儲地址來存儲,
 58     // 以此來完成擴充
 59     if (oldTab != null) {
 60         for (int j = 0; j < oldCap; ++j) {
 61             Node<K,V> e;
 62             if ((e = oldTab[j]) != null) {
 63                 oldTab[j] = null;
 64                 if (e.next == null)
 65                     newTab[e.hash & (newCap - 1)] = e;
 66                 else if (e instanceof TreeNode)
 67                     ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
 68                 else { // preserve order
 69                     Node<K,V> loHead = null, loTail = null;
 70                     Node<K,V> hiHead = null, hiTail = null;
 71                     Node<K,V> next;
 72                     do {
 73                         next = e.next;
 74                         if ((e.hash & oldCap) == 0) {
 75                             if (loTail == null)
 76                                 loHead = e;
 77                             else
 78                                 loTail.next = e;
 79                             loTail = e;
 80                         }
 81                         else {
 82                             if (hiTail == null)
 83                                 hiHead = e;
 84                             else
 85                                 hiTail.next = e;
 86                             hiTail = e;
 87                         }
 88                     } while ((e = next) != null);
 89                     if (loTail != null) {
 90                         loTail.next = null;
 91                         newTab[j] = loHead;
 92                     }
 93                     if (hiTail != null) {
 94                         hiTail.next = null;
 95                         newTab[j + oldCap] = hiHead;
 96                     }
 97                 }
 98             }
 99         }
100     }
101     return newTab;
102 }

HashMap的put方法流程總結

1、put(key, value)中直接調用了內部的putVal方法,並且先對key進行了hash操作;

2、putVal方法中,先檢查HashMap數據結構中的索引數組表是否位空,如果是的話則進行一次resize操作;

3、以HashMap索引數組表的長度減一與key的hash值進行與運算,得出在數組中的索引,如果索引指定的位置值為空,則新建一個k-v的新節點;

4、如果不滿足的3的條件,則說明索引指定的數組位置的已經存在內容,這個時候稱之碰撞出現

5、在上面判斷流程走完之后,計算HashMap全局的modCount值,以便對外部並發的迭代操作提供修改的Fail-fast判斷提供依據,於此同時增加map中的記錄數,並判斷記錄數是否觸及容量擴充的閾值,觸及則進行一輪resize操作;

6、在步驟4中出現碰撞情況時,從步驟7開始展開新一輪邏輯判斷和處理;

7、判斷key索引到的節點(暫且稱作被碰撞節點)的hash、key是否和當前待插入節點(新節點)的一致,如果是一致的話,則先保存記錄下該節點;如果新舊節點的內容不一致時,則再看被碰撞節點是否是樹(TreeNode)類型,如果是樹類型的話,則按照樹的操作去追加新節點內容;如果被碰撞節點不是樹類型,則說明當前發生的碰撞在鏈表中(此時鏈表尚未轉為紅黑樹),此時進入一輪循環處理邏輯中;

8、循環中,先判斷被碰撞節點的后繼節點是否為空,為空則將新節點作為后繼節點,作為后繼節點之后並判斷當前鏈表長度是否超過最大允許鏈表長度8,如果大於的話,需要進行一輪是否轉樹的操作;如果在一開始后繼節點不為空,則先判斷后繼節點是否與新節點相同,相同的話就記錄並跳出循環;如果兩個條件判斷都滿足則繼續循環,直至進入某一個條件判斷然后跳出循環;

9、步驟8中轉樹的操作treeifyBin,如果map的索引表為空或者當前索引表長度還小於64(最大轉紅黑樹的索引數組表長度),那么進行resize操作就行了;否則,如果被碰撞節點不為空,那么就順着被碰撞節點這條樹往后新增該新節點;

10、最后,回到那個被記住的被碰撞節點,如果它不為空,默認情況下,新節點的值將會替換被碰撞節點的值,同時返回被碰撞節點的值(V)。

put<k, v>方法流程圖

根據上面分析出的流程步驟,我大致畫了一個put方法的流程圖,以方便理解。

思考與優化

1、resize操作在當前索引表容量不足時發生,這個操作對put性能有一定的沖擊(據說還會引起死循環),但是能夠自行避免,如果在我們使用map的時候能夠知道需要存入的記錄數,則可以通過【 (記錄數 / threshold) + 1 】的方式計算出一個map的初始容量,並在聲明HashMap時將初始容量指定為這個計算值。多提一句,盡管我們按照這種方式計算出了一個能夠最大包容我們預期k-v鍵值對的容量值,但是HashMap為了性能考慮,在我們初始化容量之后,其內部又使用了一個tableSizeFor的方法將這個值轉換成了一個大於等於該值的最近的一個2次冪的數值,以方便后續其他的位操作,這個方法很巧妙,可以自行研究一下。但這個內部方法我們是不需要考慮和深究的,按照上面這個方法計算並初始化使用就行了。


免責聲明!

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



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