HashMap源碼分析——基於jdk1.8

前言:上篇文章,筆者分析了jdk1.7中HashMap的源碼,這裏將對jdk1.8的HashMap的源碼進行分析。node

注:本文jdk源碼版本爲jdk1.8.0_172算法


1.再看put操做

1  public V put(K key, V value) { 2         return putVal(hash(key), key, value, false, true); 3     }

jdk1.8中的hash算法:安全

1  static final int hash(Object key) { 2         int h; 3         // 這裏的hash算法和jdk1.7中很不同,直接高16位與低16位作異或,這樣的話高低位都進行了運算,增長hash值的隨機性
4         return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); 5     }

再看put操做的核心函數:數據結構

 1 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,  2                    boolean evict) {  3         // jdk1.8中HashMap底層數據結構使用的是Node
 4         Node<K,V>[] tab; Node<K,V> p; int n, i;  5         // 若是table還未初始化,則初始化table,注意這裏初始化使用的是resize函數[擴容函數]
 6         if ((tab = table) == null || (n = tab.length) == 0)  7             n = (tab = resize()).length;  8         /**
 9  * 這裏表示若是tab[i]位置上爲null,則直接插入數據 10  * i=(n-1)&hash與jdk1.7中找出元素在tab上的index是同樣的操做 11  * 注意這裏在多線程環境下會形成線程不安全問題 12          */
13         if ((p = tab[i = (n - 1) & hash]) == null) 14             tab[i] = newNode(hash, key, value, null); 15         else {// 若是i位置上有元素,則進行鏈式存儲
16             Node<K,V> e; K k; 17             // 若是tab[i]上的元素與插入元素的key徹底同樣,則進行覆蓋操做
18             if (p.hash == hash &&
19                     ((k = p.key) == key || (key != null && key.equals(k)))) 20                 e = p; 21             // 判斷當前元素是不是紅黑樹結構
22             else if (p instanceof TreeNode) 23                 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 24             else { 25                 for (int binCount = 0; ; ++binCount) { 26                     // 若是p節點的next爲空,則將待插入的元素,直接添加在鏈表尾
27                     if ((e = p.next) == null) { 28                         // 從這裏可知道jdk1.8在,若是存在鏈表,插入數據是直接放在鏈表尾的
29                         p.next = newNode(hash, key, value, null); 30                         // 當同一節點鏈表中元素個數>=8時,底層數據結構轉向紅黑樹,
31                         if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
32                             treeifyBin(tab, hash);  // 將底層數據結構轉向紅黑樹
33                         break; 34  } 35                     // 判斷next元素是否和插入元素相同,若是相同,則不作操做,跳出循環
36                     if (e.hash == hash &&
37                             ((k = e.key) == key || (key != null && key.equals(k)))) 38                         break; 39                     p = e; // 將next賦值給p,繼續循環
40  } 41  } 42             // 覆蓋操做
43             if (e != null) { // existing mapping for key
44                 V oldValue = e.value; 45                 // onlyIfAbsent表示是否要改變原來的值,true-不改變,false-改變
46                 if (!onlyIfAbsent || oldValue == null) 47                     e.value = value; 48  afterNodeAccess(e); 49                 return oldValue; 50  } 51  } 52         // 修改次數加1,fail-fast機制
53         ++modCount; 54         // 判斷是否須要擴容
55         if (++size > threshold) 56  resize(); 57  afterNodeInsertion(evict); 58         return null; 59     }

重點:多線程

jdk1.8中HashMap在進行put操做時:app

#1.若是同一hash值的節點數小於8時,底層數據結構仍然是鏈表;當節點數大於等於8時,會轉向紅黑樹。函數

#2.若是節點的數據結構是鏈表時,插入數據是直接放在鏈表尾的,從而避免插入元素時,造成環形鏈,形成死循環。this

put操做的核心代碼中,還涉及一個比較重要的函數,這裏進行詳細分析。spa

#resize()線程

 1 final Node<K,V>[] resize() {  2         Node<K,V>[] oldTab = table;  3         int oldCap = (oldTab == null) ? 0 : oldTab.length;  4         int oldThr = threshold;  5         int newCap, newThr = 0;  6         // 若是table中有元素
 7         if (oldCap > 0) {  8             // 容量是否已達限制
 9             if (oldCap >= MAXIMUM_CAPACITY) {  10                 threshold = Integer.MAX_VALUE;  11                 return oldTab;  12  }  13             // 擴容,並更新擴容閾值
 14             else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
 15                     oldCap >= DEFAULT_INITIAL_CAPACITY)  16                 newThr = oldThr << 1; // double threshold
 17  }  18         // 若是table中沒有元素,可是已初始化擴容閾值,這裏將table的新容量賦值爲擴容閾值
 19         else if (oldThr > 0) // initial capacity was placed in threshold
 20             newCap = oldThr;  21             // 若是以上條件都不知足,則利用默認值進行初始化
 22         else {               // zero initial threshold signifies using defaults
 23             newCap = DEFAULT_INITIAL_CAPACITY;  24             newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);  25  }  26         // 這裏再次對擴容閾值進行判斷,若是未初始化,則進行初始化
 27         if (newThr == 0) {  28             float ft = (float)newCap * loadFactor;  29             newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
 30                     (int)ft : Integer.MAX_VALUE);  31  }  32         threshold = newThr;  33         @SuppressWarnings({"rawtypes","unchecked"})  34         Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];  35         table = newTab;  36         // 若是原來table有值,則循環將原值轉移到newTab中
 37         if (oldTab != null) {  38             for (int j = 0; j < oldCap; ++j) {  39                 Node<K,V> e;  40                 // 找到有值的節點
 41                 if ((e = oldTab[j]) != null) {  42                     oldTab[j] = null; //將原來table中當前位置置null
 43                     if (e.next == null) // 若是當前節點next爲null,將其放置在newTab中的新位置
 44                         newTab[e.hash & (newCap - 1)] = e;  45                         // 若是是紅黑樹則進行紅黑樹操做,關於紅黑樹後面會進行分析
 46                     else if (e instanceof TreeNode)  47                         ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);  48                     else { // preserve order // 當走到這裏,說明節點上爲鏈表形式存儲數據,需進行循環操做  49                         // 存儲位置在newTable和oldTable位置不變元素
 50                         Node<K,V> loHead = null, loTail = null;  51                         // 存儲oldTable中位置發生了變化的元素,固然這裏是和oldTable相比較  52                         // 參看下面的註釋,應該能夠很好理解
 53                         Node<K,V> hiHead = null, hiTail = null;  54                         Node<K,V> next;  55                         do {  56                             // 因爲是鏈表循環,所以需存儲next節點的值,這種形式在jdk1.7中出現過屢次
 57                             next = e.next;  58                             /**
 59  *這裏須要注意一下,這裏是用元素的hash值,與原來table長度作&操做  60  * 若是爲0,則表示e.hash&(newCap-1)和e.hash&(oldCap-1)是同樣的  61  * 也就說元素的位置在newTable中是不變的,由於newTable的大小爲oldTable大小的2倍  62  * 至關於其二進制向左移動了1位,其newCap-1的二進制全都爲1,且比原來oldCap-1的二進制多了一個1  63  * eg:oldCap=16,newCap=32,注意求key的位置是用e.hash&(table.length-1)  64  * e.hash&0x1111=原來key的位置  65  * e.hash&0x10000=0,代表e.hash在二進制的第5位上必定爲0,因此:  66  * e.hash&0x11111=也必定是原來key的位置  67  * 若是:  68  * e.hash&0x10000=1,代表e.hash在二進制的第5位上必定爲1,因此:  69  * e.hash&0x11111=原來key的位置加上oldCap的長度便可(0x10000)  70  * 這樣根據一個二進制位就將原來的一條鏈表分紅兩條鏈表進行存儲,這裏很是的關鍵,不是很好理解  71  * 仔細理解上面的解釋,相信你會發現這是很是神奇的一個技巧  72                              */
 73                             // 有了上面的原理,再來看這就很是明確了  74                             // 元素在newTable中位置不改變
 75                             if ((e.hash & oldCap) == 0) {  76                                 // 初始時,將e放在loHead頭上,而後尾又是e,後續循環的時候,只操做tail就好了,造成鏈表
 77                                 if (loTail == null)  78                                     loHead = e;  79                                 else
 80                                     loTail.next = e;  81                                 // 尾部存儲爲e,造成鏈表,注意理解就好
 82                                 loTail = e;  83  }  84                             // 元素在newTable中位置發生了變化[相對oldTable]  85                             // 這裏就至關於兩條鏈表了,位置不變的一條,位置變了的又是一條
 86                             else {  87                                 if (hiTail == null)  88                                     hiHead = e;  89                                 else
 90                                     hiTail.next = e;  91                                 hiTail = e;  92  }  93                         } while ((e = next) != null);  94                         // 若是位置不變鏈表不爲null
 95                         if (loTail != null) {  96                             loTail.next = null;  97                             // 從這裏也可看出這裏存儲的是元素在newTable中位置不改變[相對oldTable]  98                             // 只須要存儲head值便可,由於已造成鏈表
 99                             newTab[j] = loHead; 100  } 101                         if (hiTail != null) { 102                             hiTail.next = null; 103                             // 位置變化的元素,位置只須要加上oldCap的值就能夠了,上面已進行分析
104                             newTab[j + oldCap] = hiHead; 105  } 106  } 107  } 108  } 109  } 110         return newTab; 111     }

重點:

resize函數中在對節點元素存在鏈表時的處理有點小技巧,雖然是再次hash,但它是根據key在oldTable中位置與newTable的位置來進行區分的,變成兩條鏈表存儲,具體分析過程在函數中已註釋寫得很是詳細。

jdk1.8中HashMap主要在底層增長了紅黑樹的數據結構,關於紅黑樹這種數據結構筆者仍是有點懵懂,後面會專門深刻這方面的知識點。

2.再看get操做

1 public V get(Object key) { 2         // 這裏與jdk1.7的get方法有很大的不同
3         Node<K,V> e; 4         // 經過getNode方法,若是e不爲null,則返回e的value,不然返回null
5         return (e = getNode(hash(key), key)) == null ? null : e.value; 6     }

get關鍵函數getNode:

 1 final Node<K,V> getNode(int hash, Object key) {  2         Node<K,V>[] tab; Node<K,V> first, e; int n; K k;  3         // 若是HashMap中有值,其當前元素在table中有值,則進行尋找
 4         if ((tab = table) != null && (n = tab.length) > 0 &&
 5             (first = tab[(n - 1) & hash]) != null) {  6             // 註釋已經說的很是清楚了,檢查第一個節點的值是否與要查找的相同,快速return
 7             if (first.hash == hash && // always check first node
 8                 ((k = first.key) == key || (key != null && key.equals(k))))  9                 return first; 10             // 若是當前節點下存在紅黑樹或鏈表結構,則進行循環操做 
11             if ((e = first.next) != null) { 12                 // 若是next節點爲紅黑樹則在紅黑樹中查找元素
13                 if (first instanceof TreeNode) 14                     return ((TreeNode<K,V>)first).getTreeNode(hash, key); 15                 // 走到該分支,則代表當前節點下爲鏈表結構,下面的循環就比較簡單了,經過循環不斷的查找相同的元素,有則返回,無則返回null
16                 do { 17                     if (e.hash == hash &&
18                         ((k = e.key) == key || (key != null && key.equals(k)))) 19                         return e; 20                 } while ((e = e.next) != null); 21  } 22  } 23         // 不然直接返回null
24         return null; 25     }

分析:

#1.get函數的核心代碼邏輯仍是很是簡單的,注意:老是首先校驗頭結點,快速return

#2.其次是紅黑樹中查找元素,因爲紅黑樹的數據結構相對較複雜,後續在進行相應分析。

3.其餘重要函數

#removeNode該函數爲remove函數的核心函數:

 1 /**
 2  * HashMap刪除元素  3  * @param hash key的hash值  4  * @param key 傳入的刪除的key值  5  * @param value 值,傳入爲nul  6  * @param matchValue if true only remove if value is equal 若是爲true,則只移除value相等的元素,因此這裏傳入false  7  * @param movable if false do not move other nodes while removing 該值主要用於紅黑樹移除節點後,再重塑紅黑樹,因此傳入true  8  * @return
 9      */
10     final Node<K,V> removeNode(int hash, Object key, Object value, 11                                boolean matchValue, boolean movable) { 12         Node<K,V>[] tab; Node<K,V> p; int n, index; 13         // 一樣先判斷是否存在該元素,並記錄頭結點元素p
14         if ((tab = table) != null && (n = tab.length) > 0 &&
15                 (p = tab[index = (n - 1) & hash]) != null) { 16             Node<K,V> node = null, e; K k; V v; 17             // 若是頭結點直接命中,則用node存儲下來
18             if (p.hash == hash &&
19                     ((k = p.key) == key || (key != null && key.equals(k)))) 20                 node = p; 21             // 該分支表示頭結點未命中,判斷p結點的next是否不爲null,由於要循環,因此須要將next元素記錄下來,這種方式在HashMap的循環中很經常使用
22             else if ((e = p.next) != null) { 23                 // 若是p爲紅黑樹結構,則走紅黑樹分支,找到要刪除的結點
24                 if (p instanceof TreeNode) 25                     node = ((TreeNode<K,V>)p).getTreeNode(hash, key); 26                 // 不然進行循環查找
27                 else { 28                     do { 29                         // 若是命中,則跳出循環
30                         if (e.hash == hash &&
31                                 ((k = e.key) == key ||
32                                         (key != null && key.equals(k)))) { 33                             node = e; // node結點保存命中的結點元素
34                             break; 35  } 36                         // 若是未命中,則用p記錄next結點,爲後續刪除作準備,p表示要刪除節點的前一個節點,由於這裏有e=e.next操做,與jdk1.7相同。
37                         p = e; 38                     } while ((e = e.next) != null); 39  } 40  } 41             // 要刪除結點不爲null,&& 操做後:注意這裏!matchValue,由於傳入值爲false,因此這裏一直爲true
42             if (node != null && (!matchValue || (v = node.value) == value ||
43                     (value != null && value.equals(v)))) { 44                 // 若是要刪除節點爲紅黑樹,則走紅黑樹分支
45                 if (node instanceof TreeNode) 46                     ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable); 47                 // 若是要刪除節點與p相同,則說明是頭結點,則直接將tab[index]位置指向node.next,這樣就踢出了node元素,即刪除
48                 else if (node == p) 49                     tab[index] = node.next; 50                 else // 若是不相同,則直接將p.next指向node.next,一樣跳過了node,也就刪除了。
51                     p.next = node.next; 52                 ++modCount; //操做記錄+1,fail-fast機制
53                 --size;// 元素個數減1
54  afterNodeRemoval(node); 55                 return node; 56  } 57  } 58         // 上述都未找到,則直接返回null
59         return null; 60     }

重點:

刪除函數與jdk1.7中原理同樣,都是經過操做一個結點元素進行刪除,只是若是結點爲紅黑樹,需走紅黑樹分支。

總結

這裏比較粗略的分析了jdk1.8中HashMap的源碼,它與jdk1.7中源碼的原理大體相同,這裏總結其重點:

#1.底層引入了紅黑樹數據結構,在添加元素時,若是table位置上的元素數量>=8時,則當前位置結點數據結構會轉向紅黑樹;

當table位置上元素數量<=6時,數據結構又會轉換成鏈表(在resize中,紅黑樹分支)。

#2.改變了hash算法,直接高16位與低16位作異或。

#3.resize函數中,在作再次hash時,用兩條鏈表分散存儲節點,而且避免了jdk1.7中的死循環狀況。

#4.一樣存在fail-fast機制。


by Shawn Chen,2019.03.09,晚。

相關文章
相關標籤/搜索