前言:上篇文章,筆者分析了jdk1.7中HashMap的源碼,這裏將對jdk1.8的HashMap的源碼進行分析。node
注:本文jdk源碼版本爲jdk1.8.0_172算法
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主要在底層增長了紅黑樹的數據結構,關於紅黑樹這種數據結構筆者仍是有點懵懂,後面會專門深刻這方面的知識點。
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.其次是紅黑樹中查找元素,因爲紅黑樹的數據結構相對較複雜,後續在進行相應分析。
#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,晚。