【Java】HashMap源碼分析——常用方法詳解
阿新 • • 發佈:2018-10-13
fir 設置 直接 dfa 構造方法 change mage null 這也
上一篇介紹了HashMap的基本概念,這一篇著重介紹HasHMap中的一些常用方法:
put()
get()
**resize()**
首先介紹resize()這個方法,在我看來這是HashMap中一個非常重要的方法,是用來調整HashMap中table的容量的,在很多操作中多需要重新計算容量。
源碼如下:
1 final Node<K,V>[] resize() { 2 Node<K,V>[] oldTab = table; 3 int oldCap = (oldTab == null) ? 0 : oldTab.length; 4 intoldThr = threshold; 5 int newCap, newThr = 0; 6 if (oldCap > 0) { 7 if (oldCap >= MAXIMUM_CAPACITY) { 8 threshold = Integer.MAX_VALUE; 9 return oldTab; 10 } 11 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && 12oldCap >= DEFAULT_INITIAL_CAPACITY) 13 newThr = oldThr << 1; // double threshold 14 } 15 else if (oldThr > 0) // initial capacity was placed in threshold 16 newCap = oldThr; 17 else { // zero initial threshold signifies using defaults18 newCap = DEFAULT_INITIAL_CAPACITY; 19 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); 20 } 21 if (newThr == 0) { 22 float ft = (float)newCap * loadFactor; 23 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? 24 (int)ft : Integer.MAX_VALUE); 25 } 26 threshold = newThr; 27 @SuppressWarnings({"rawtypes","unchecked"}) 28 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; 29 table = newTab; 30 if (oldTab != null) { 31 for (int j = 0; j < oldCap; ++j) { 32 Node<K,V> e; 33 if ((e = oldTab[j]) != null) { 34 oldTab[j] = null; 35 if (e.next == null) 36 newTab[e.hash & (newCap - 1)] = e; 37 else if (e instanceof TreeNode) 38 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); 39 else { // preserve order 40 Node<K,V> loHead = null, loTail = null; 41 Node<K,V> hiHead = null, hiTail = null; 42 Node<K,V> next; 43 do { 44 next = e.next; 45 if ((e.hash & oldCap) == 0) { 46 if (loTail == null) 47 loHead = e; 48 else 49 loTail.next = e; 50 loTail = e; 51 } 52 else { 53 if (hiTail == null) 54 hiHead = e; 55 else 56 hiTail.next = e; 57 hiTail = e; 58 } 59 } while ((e = next) != null); 60 if (loTail != null) { 61 loTail.next = null; 62 newTab[j] = loHead; 63 } 64 if (hiTail != null) { 65 hiTail.next = null; 66 newTab[j + oldCap] = hiHead; 67 } 68 } 69 } 70 } 71 } 72 return newTab; 73 }
可以看到這段代碼非常龐大,其內容可以分為兩大部分:
第一部分計算並生成新的哈希表(空表):
1 // 記錄原表 2 Node<K,V>[] oldTab = table; 3 // 得到原來哈希表的總長度,及原來總容量 4 int oldCap = (oldTab == null) ? 0 : oldTab.length; 5 // 得到原來最佳容量 6 int oldThr = threshold; 7 // 存放新的總容量、新最佳容量的變量 8 int newCap, newThr = 0; 9 if (oldCap > 0) { 10 // 原來總容量達到或超過HashMap的最大容量,則最佳容量設置為int類型的最大值 11 // 且原來容量不變,直接返回,不做後需調整 12 if (oldCap >= MAXIMUM_CAPACITY) { 13 threshold = Integer.MAX_VALUE; 14 return oldTab; 15 } 16 // 讓新的總容量等於原來容量的二倍 17 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && 18 oldCap >= DEFAULT_INITIAL_CAPACITY) 19 // 新的最佳容量也變為原來的二倍 20 newThr = oldThr << 1; 21 } 22 // 原來總容量為0,將新的總容量設置為最佳容量,構造方法出入參數是一個派生的Map的時候,就會使用派生的Map計算出新的最佳容量 23 else if (oldThr > 0) 24 newCap = oldThr; 25 else { 26 // 原來總容量和原來最佳容量都沒有定義 27 // 新的總容量設為默認值16 28 // 新的最佳容量=默認負載因子×默認容量=0.75×16=12 29 newCap = DEFAULT_INITIAL_CAPACITY; 30 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); 31 } 32 // 判斷上述操作後新的最佳容量是否計算,若沒有,就利用負載因子和新的總容量計算 33 if (newThr == 0) { 34 float ft = (float)newCap * loadFactor; 35 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? 36 (int)ft : Integer.MAX_VALUE); 37 } 38 // 更新當前的最佳容量 39 threshold = newThr; 40 @SuppressWarnings({"rawtypes","unchecked"}) 41 // 生成新的哈希表,即一維數組 42 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; 43 // 更新哈希表 44 table = newTab;
可以看出上述操作僅僅是生成了一張大小合適的哈希表,但表還是空的,後面的操作就是把以前的表中的元素重新排列,移動到當前表中合適的位置!
第二部分將原表元素移動到新表合適的位置:
1 // 先判斷原表是或否為空 2 if (oldTab != null) { 3 // 遍歷原表(一維數組)中的所有元素, 4 for (int j = 0; j < oldCap; ++j) { 5 // 記錄原來一維數組中下標為j的元素 6 Node<K,V> e; 7 // 只對有效元素進行操作 8 if ((e = oldTab[j]) != null) { 9 //將原表中的元素置空 10 oldTab[j] = null; 11 if (e.next == null) 12 // 當前元素沒有後繼,那麽直接把它放在新表中合適位置 13 // 其中e.hash & (newCap - 1)在我上一篇博客有介紹 14 // 就是以該節點的hash值和新表總容量取余,將余數作為下標 15 newTab[e.hash & (newCap - 1)] = e; 16 else if (e instanceof TreeNode) 17 // 當前元素有後繼,且後繼是紅黑樹 18 // 進行有關紅黑樹的相應操作 19 // 這裏不詳細介紹紅黑樹的操作 20 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); 21 else { 22 // 這裏就進行有關鏈表的移動 23 // 這兩組結點變量,分別代表兩條不同鏈表的頭和尾 24 // 低位的頭和尾 25 Node<K,V> loHead = null, loTail = null; 26 // 高位的頭和尾 27 Node<K,V> hiHead = null, hiTail = null; 28 // 下一節點 29 Node<K,V> next; 30 do { 31 // 讓next等於當前結點的後繼結點 32 next = e.next; 33 // 這個位運算實際上判斷的是該節點在新表中的位置是否發生改變 34 // 成立則說明沒有改變,還是原來表中下標為j的位置 35 if ((e.hash & oldCap) == 0) { 36 // 若是首結點,則讓低位的頭等於當前結點 37 if (loTail == null) 38 loHead = e; 39 else 40 // 若不是首結點,則讓低位的尾等於當前結點 41 loTail.next = e; 42 // 讓低位的尾移動到當前 43 loTail = e; 44 } 45 // 這裏就說明其在新表中的位置發生了改變,則要將其放入另一條鏈表 46 else { 47 // 若是首結點,則讓高位的頭等於當前結點 48 if (hiTail == null) 49 hiHead = e; 50 else 51 // 若不是首結點,則讓高位的尾等於當前結點 52 hiTail.next = e; 53 // 讓高位的尾移動到當前 54 hiTail = e; 55 } 56 } while ((e = next) != null); 57 // 原來位置的這條鏈表還存在 58 if (loTail != null) { 59 // 置空低位的尾的next 60 loTail.next = null; 61 // 將該鏈表的頭結點放入新表下標為j的位置,即原表中的原位置 62 newTab[j] = loHead; 63 } 64 // 新位置上的鏈表存在 65 if (hiTail != null) { 66 // 置空高位的尾的next 67 hiTail.next = null; 68 // 將該鏈表的頭結點放入新表中下標為j+原表長度的位置 69 newTab[j + oldCap] = hiHead; 70 } 71 } 72 } 73 } 74 } 75 return newTab;
鏈表的移動如圖:
可以看出,這個方法可以使得單個結點重新散列,鏈表可以拆分成兩條,紅黑樹重新移動,這樣使得新的哈希表分布比以前均勻!
下面來分析put方法:
源碼如下:
1 public V put(K key, V value) { 2 return putVal(hash(key), key, value, false, true); 3 }
這裏我們可以知道其調用了內部的一個putVal方法:
首先第一個參數是通過內部的hash方法(在前一篇博客有介紹過)計算出鍵對象的hash(int類型)值,再把key和value對象傳過去,置於後面兩個參數先不著急
先來看下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 * // 看以看出,put方法傳入的onlyIfAbsent是false,那麽就會改變原來已存在的值 8 * @param onlyIfAbsent if true, don‘t change existing value 9 * // 這個參數先不考慮,往後慢慢分析 10 * @param evict if false, the table is in creation mode. 11 * @return previous value, or null if none 12 */ 13 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)
該方法內容:
1 // 用於保存原表 2 Node<K,V>[] tab; 3 // 保存下標為hash的結點 4 Node<K,V> p; 5 // n用來記錄表長 6 int n, i; 7 // 先檢查原表是否存在,或者是空表 8 if ((tab = table) == null || (n = tab.length) == 0) 9 // 如果為空就生成一張大小為16的新表 10 n = (tab = resize()).length; 11 if ((p = tab[i = (n - 1) & hash]) == null) 12 // 如果以該方法形參hash對表長取余,令其作為下標的表中的元素為空,那麽就產生一個新結點放在這個位置 13 tab[i] = newNode(hash, key, value, null); 14 else { 15 // 如果該結點不空,那麽就會出現兩種情況:鏈表和紅黑樹 16 Node<K,V> e; K k; 17 if (p.hash == hash && 18 ((k = p.key) == key || (key != null && key.equals(k)))) 19 // 如果當前結點的hash並且key值(指針值和內容值)相等,由於onlyIfAbsent是false,那麽就會改變這個結點的V值,先用e將其保存起來 20 e = p; 21 else if (p instanceof TreeNode) 22 // 如果當前結點是一棵紅黑樹,那麽就進行紅黑樹的平衡,這裏不討論紅黑樹的問題 23 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 24 else { 25 // 這裏就對鏈表進行操作 26 // 從頭開始遍歷這條鏈表 27 for (int binCount = 0; ; ++binCount) { 28 if ((e = p.next) == null) { 29 // 如果該節點的next為空 30 // 就需要新增一個結點追加其後 31 p.next = newNode(hash, key, value, null); 32 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 33 // 這裏進行紅黑樹閾值的判斷,由於TREEIFY_THRESHOLD默認值是8,binCount是從0開始,那麽當鏈表長度大於等於8的時候,就將該鏈表轉換成紅黑樹,並且結束循環 34 treeifyBin(tab, hash); 35 break; 36 } 37 // 這裏和之前的判斷是一樣的 38 if (e.hash == hash && 39 ((k = e.key) == key || (key != null && key.equals(k)))) 40 break; 41 // 讓p = p->next 42 p = e; 43 } 44 } 45 // 若e非空,則就是說明原表中存在hash值相等,且key的值或內容相同的結點 46 if (e != null) { 47 // 將原來的V值保存 48 V oldValue = e.value; 49 // 判斷是否是需要進行覆蓋原來V值的操作 50 if (!onlyIfAbsent || oldValue == null) 51 // 覆蓋原來的V值 52 e.value = value; 53 // 這個方法是一個空的方法,預留的一個操作,不用去管它 54 afterNodeAccess(e); 55 // 由於在這裏面的操作只是替換了原來的V值,並沒有改變原來表的大小,直接返回oldValue 56 return oldValue; 57 } 58 } 59 // 操作數自增 60 ++modCount; 61 // 實際大小自增 62 // 若其大於最佳容量進行擴容的操作,使其分布均勻 63 if (++size > threshold) 64 resize(); 65 // 這也是一個空的方法,預留操作 66 afterNodeInsertion(evict); 67 // 並沒有替換原來的V值,返回null 68 return null;
下來是get方法,邏輯相對簡單不難分析:
1 public V get(Object key) { 2 Node<K,V> e; 3 return (e = getNode(hash(key), key)) == null ? null : e.value; 4 }
同樣也是通過hash方法計算出key對象的hash值,調用內部的getNode方法:
1 final Node<K,V> getNode(int hash, Object key) { 2 // 記錄表對象 3 Node<K,V>[] tab; 4 // 記錄第一個結點和當前節點 5 Node<K,V> first, e; 6 // 記錄表長 7 int n; 8 // 記錄K值 9 K k; 10 // 表非空或者長度大於0才對其操作 11 // 並且key的hash值對表長取余為下標,其所對應的哈希表中的結點存在 12 if ((tab = table) != null && (n = tab.length) > 0 && 13 (first = tab[(n - 1) & hash]) != null) { 14 // 當前結點滿足情況,直接返回給該節點 15 if (first.hash == hash && 16 ((k = first.key) == key || (key != null && key.equals(k)))) 17 return first; 18 // 後面就分為兩種情況:在紅黑樹或者鏈表中查找 19 if ((e = first.next) != null) { 20 // 當前結點是紅黑樹,進行紅黑樹的查找 21 if (first instanceof TreeNode) 22 return ((TreeNode<K,V>)first).getTreeNode(hash, key); 23 // 進行鏈表的遍歷 24 do { 25 if (e.hash == hash && 26 ((k = e.key) == key || (key != null && key.equals(k)))) 27 return e; 28 } while ((e = e.next) != null); 29 } 30 } 31 return null; 32 }
若有不足還請指出!
我在CSDN也放了一篇【Java】HashMap源碼分析——常用方法詳解
【Java】HashMap源碼分析——常用方法詳解