HashMap中resize()剖析java
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; // 只有非第一次擴容纔會進來(第一次擴容在第一次put) if (oldCap > 0) { // oldCap最大爲MAXIMUM_CAPACITY(2^30),可查看帶參構造方法① if (oldCap >= MAXIMUM_CAPACITY) { /** * threshold變成MAX_VALUE(2^31-1),隨它們碰撞。可是oldCap不改變, * 由於若是oldCap翻倍就爲負數了,若是賦值爲MAX_VALUE, * 參考 Map容量爲何不能爲MAX_VALUE② */ threshold = Integer.MAX_VALUE; return oldTab; } // 容量翻倍 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) /** * 爲何須要判斷oldCap >= DEFAULT_INITIAL_CAPACITY呢? * 應該是容量較小時 capacity * loadFactor形成的偏差比較大, * 例如初始化容量爲2 threshold則爲1,若是每次擴容threshold都翻倍, * 那負載因子是0.5了。 * 爲何只小於16呢? * 我猜想是在每次擴容都計算threshold和用位運算翻倍之間作權衡 */ newThr = oldThr << 1; } // 帶參初始化會進入這裏,主要是爲了從新算threshold else if (oldThr > 0) newCap = oldThr; // 不帶參初始化會進入這裏 else { newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 從新算threshold if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; // 擴容 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; // 複製數據到新table中 if (oldTab != null) { // 遍歷Node for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { oldTab[j] = null; // 若是隻有一個節點,則直接賦值 if (e.next == null) newTab[e.hash & (newCap - 1)] = e; // 若是是紅黑樹(較爲複雜,不在這裏說明) else if (e instanceof TreeNode) ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); else { // 之因此定義兩個頭兩個尾對象,是因爲鏈表中的元素的下標在擴容後,要麼是原下標+oldCap,要麼不變,下面會證明 Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null; Node<K,V> next; //遍歷桶中的鏈表 do { next = e.next; // 下標沒有改變,參考③ if ((e.hash & oldCap) == 0) { if (loTail == null) // 第一個節點 loHead = e; else // 加入到尾部 loTail.next = e; // 調整尾部元素 loTail = e; } // 下標改變 else { // 同理 if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e; } } while ((e = next) != null); // 原下標對應的鏈表 if (loTail != null) { // 尾部節點next設置爲null,代碼嚴謹 loTail.next = null; newTab[j] = loHead; } // 新下標對應的鏈表 if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; } ①帶參構造方法 public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); // 容量最大爲MAXIMUM_CAPACITY(2^30) if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; // threshold初始化爲最接近initialCapacity的2的冪次方,而且大於或等於initialCapacity。可是在第一次put的時候,threshold會變成threshold * loadFactor this.threshold = tableSizeFor(initialCapacity); } ②Map容量爲何不能爲MAX_VALUE 該爲題可轉爲:爲何在Java1.8,每次擴容都爲2的冪次方呢? // 計算下標,下面是map的put和get中都用到計算下標的 (n - 1) & hash 當容量爲MAX_VALUE(2^31-1)時,轉換成二進制 hash & 0111 1111 1111 1111 1111 1111 1111 1110 ----------------------------------------------- xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx0 從上面可看出最低位不管hash是任何值時,都爲0,也就是下標只有2^30種可能,有2^30-1個下標沒有被使用 因此當容量爲MAX_VALUE(2^31-1)時會形成一半的空間浪費,效率等同於MAXIMUM_CAPACITY(2^30) ③e.hash & oldCap 該步驟是爲了計算位置是否須要移動 由於oldTab的元素下標是根據 hash(key) & (oldCap-1) 計算的,若是擴容後,計算下標是 hash(key) & (2*oldCap-1) 換成二進制就比較清晰了
其中看出低位和高位的亦或主要是是hash分佈均勻。數組
treeifyBin方法,應該能夠解釋爲:把容器裏的元素變成樹結構。當HashMap的內部元素數組中某個位置上存在多個hash值相同的鍵值對,這些Node已經造成了一個鏈表,當該鏈表的長度大於等於9this
/** * tab:元素數組, * hash:hash值(要增長的鍵值對的key的hash值) */ final void treeifyBin(Node<K,V>[] tab, int hash) { int n, index; Node<K,V> e; /* * 若是元素數組爲空 或者 數組長度小於 樹結構化的最小限制 * MIN_TREEIFY_CAPACITY 默認值64,對於這個值能夠理解爲:若是元素數組長度小於這個值,沒有必要去進行結構轉換 * 當一個數組位置上集中了多個鍵值對,那是由於這些key的hash值和數組長度取模以後結果相同。(並非由於這些key的hash值相同) * 由於hash值相同的機率不高,因此能夠經過擴容的方式,來使得最終這些key的hash值在和新的數組長度取模以後,拆分到多個數組位置上。 */ if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) resize(); // 擴容,可參見resize方法解析 // 若是元素數組長度已經大於等於了 MIN_TREEIFY_CAPACITY,那麼就有必要進行結構轉換了 // 根據hash值和數組長度進行取模運算後,獲得鏈表的首節點 else if ((e = tab[index = (n - 1) & hash]) != null) { TreeNode<K,V> hd = null, tl = null; // 定義首、尾節點 do { TreeNode<K,V> p = replacementTreeNode(e, null); // 將該節點轉換爲 樹節點 if (tl == null) // 若是尾節點爲空,說明尚未根節點 hd = p; // 首節點(根節點)指向 當前節點 else { // 尾節點不爲空,如下兩行是一個雙向鏈表結構 p.prev = tl; // 當前樹節點的 前一個節點指向 尾節點 tl.next = p; // 尾節點的 後一個節點指向 當前節點 } tl = p; // 把當前節點設爲尾節點 } while ((e = e.next) != null); // 繼續遍歷鏈表 // 到目前爲止 也只是把Node對象轉換成了TreeNode對象,把單向鏈表轉換成了雙向鏈表 // 把轉換後的雙向鏈表,替換原來位置上的單向鏈表 if ((tab[index] = hd) != null) hd.treeify(tab);//此處單獨解析 } }
後續部分繼續補充。
參考博客:https://blog.csdn.net/weixin_42340670/article/details/80503863
https://blog.csdn.net/u010828343/article/details/80769385.net