系列文章目錄java
上一篇咱們說明了HashMap的構造函數, 談到構造函數中並不會初始化table
變量, table
變量是在 resize
過程當中初始化的.node
本篇咱們就來聊聊HashMap的擴容: resizesegmentfault
本文的源碼基於 jdk8 版本.數組
resize用於如下兩種狀況之一框架
下面咱們直接來對照源碼分析:函數
final Node<K,V>[] resize() { Node<K,V>[] oldTab = table; int oldCap = (oldTab == null) ? 0 : oldTab.length; int oldThr = threshold; int newCap, newThr = 0; // 原table中已經有值 if (oldCap > 0) { // 已經超過最大限制, 再也不擴容, 直接返回 if (oldCap >= MAXIMUM_CAPACITY) { threshold = Integer.MAX_VALUE; return oldTab; } // 注意, 這裏擴容是變成原來的兩倍 // 可是有一個條件: `oldCap >= DEFAULT_INITIAL_CAPACITY` else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) newThr = oldThr << 1; // double threshold } // 在構造函數一節中咱們知道 // 若是沒有指定initialCapacity, 則不會給threshold賦值, 該值被初始化爲0 // 若是指定了initialCapacity, 該值被初始化成大於initialCapacity的最小的2的次冪 // 這裏是指, 若是構造時指定了initialCapacity, 則用threshold做爲table的實際大小 else if (oldThr > 0) // initial capacity was placed in threshold newCap = oldThr; // 若是構造時沒有指定initialCapacity, 則用默認值 else { // zero initial threshold signifies using defaults newCap = DEFAULT_INITIAL_CAPACITY; newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); } // 計算指定了initialCapacity狀況下的新的 threshold if (newThr == 0) { float ft = (float)newCap * loadFactor; newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); } threshold = newThr; //從以上操做咱們知道, 初始化HashMap時, //若是構造函數沒有指定initialCapacity, 則table大小爲16 //若是構造函數指定了initialCapacity, 則table大小爲threshold, 即大於指定initialCapacity的最小的2的整數次冪 // 從下面開始, 初始化table或者擴容, 實際上都是經過新建一個table來完成的 @SuppressWarnings({"rawtypes","unchecked"}) Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; table = newTab; // 下面這段就是把原來table裏面的值所有搬到新的table裏面 if (oldTab != null) { for (int j = 0; j < oldCap; ++j) { Node<K,V> e; if ((e = oldTab[j]) != null) { // 這裏注意, table中存放的只是Node的引用, 這裏將oldTab[j]=null只是清除舊錶的引用, 可是真正的node節點還在, 只是如今由e指向它 oldTab[j] = null; // 若是該存儲桶裏面只有一個bin, 就直接將它放到新表的目標位置 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 { // preserve order 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) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; } } } } } return newTab; }
下面咱們單獨來看看這段設計的很精妙的代碼工具
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) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; }
首先咱們看源碼時要抓住一個大框架, 不要被它複雜的流程唬住, 咱們一段一段來看:源碼分析
Node<K,V> loHead = null, loTail = null; Node<K,V> hiHead = null, hiTail = null;
上面這段定義了四個Node的引用, 從變量命名上,咱們初步猜想, 這裏定義了兩個鏈表, 咱們把它稱爲 lo鏈表
和 hi鏈表
, loHead
和 loTail
分別指向 lo鏈表
的頭節點和尾節點, hiHead
和 hiTail
以此類推.this
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);
上面這段是一個do-while循環, 咱們先從中提取出主要框架:spa
do { next = e.next; ... } while ((e = next) != null);
從上面的框架上來看, 就是在按順序遍歷該存儲桶位置上的鏈表中的節點.
咱們再看if-else
語句的內容:
// 插入lo鏈表 if (loTail == null) loHead = e; else loTail.next = e; loTail = e; // 插入hi鏈表 if (hiTail == null) hiHead = e; else hiTail.next = e; hiTail = e;
上面結構相似的兩段看上去就是一個將節點e插入鏈表的動做.
最後再加上 if
塊, 則上面這段的目的就很清晰了:
咱們首先準備了兩個鏈表lo
和hi
, 而後咱們順序遍歷該存儲桶上的鏈表的每一個節點, 若是(e.hash & oldCap) == 0
, 咱們就將節點放入lo
鏈表, 不然, 放入hi
鏈表.
第二段弄明白以後, 咱們再來看第三段:
if (loTail != null) { loTail.next = null; newTab[j] = loHead; } if (hiTail != null) { hiTail.next = null; newTab[j + oldCap] = hiHead; }
這一段看上去就很簡單了:
若是lo鏈表非空, 咱們就把整個lo鏈表放到新table的j
位置上
若是hi鏈表非空, 咱們就把整個hi鏈表放到新table的j+oldCap
位置上
綜上咱們知道, 這段代碼的意義就是將原來的鏈表拆分紅兩個鏈表, 並將這兩個鏈表分別放到新的table的 j
位置和 j+oldCap
上, j
位置就是原鏈表在原table中的位置, 拆分的標準就是:
(e.hash & oldCap) == 0
爲了幫助你們理解,我畫了個示意圖:
(ps: 畫個圖真的好累啊, 你們有什麼好的畫圖工具推薦嗎?)
(e.hash & oldCap) == 0
j
以及 j+oldCap
上面咱們已經弄懂了鏈表拆分的代碼, 可是這個拆分條件看上去很奇怪, 這裏咱們來稍微解釋一下:
首先咱們要明確三點:
(n - 1) & hash
其實就是取hash的低m
位例如:
咱們假設 oldCap = 16, 即 2^4,
16 - 1 = 15, 二進制表示爲 0000 0000 0000 0000 0000 0000 0000 1111
可見除了低4位, 其餘位置都是0(簡潔起見,高位的0後面就不寫了), 則 (16-1) & hash
天然就是取hash值的低4位,咱們假設它爲 abcd
.
以此類推, 當咱們將oldCap擴大兩倍後, 新的index的位置就變成了 (32-1) & hash
, 其實就是取 hash值的低5位. 那麼對於同一個Node, 低5位的值無外乎下面兩種狀況:
0abcd 1abcd
其中, 0abcd
與原來的index值一致, 而1abcd
= 0abcd + 10000
= 0abcd + oldCap
故雖然數組大小擴大了一倍,可是同一個key
在新舊table中對應的index卻存在必定聯繫: 要麼一致,要麼相差一個 oldCap
。
而新舊index是否一致就體如今hash值的第4位(咱們把最低爲稱做第0位), 怎麼拿到這一位的值呢, 只要:
hash & 0000 0000 0000 0000 0000 0000 0001 0000
上式就等效於
hash & oldCap
故得出結論:
若是(e.hash & oldCap) == 0
則該節點在新表的下標位置與舊錶一致都爲j
若是(e.hash & oldCap) == 1
則該節點在新表的下標位置j + oldCap
根據這個條件, 咱們將原位置的鏈表拆分紅兩個鏈表, 而後一次性將整個鏈表放到新的Table對應的位置上.
怎麼樣? 這個設計是否是很巧妙, 反正LZ是無比佩服源碼做者的!
threshold
值的時候, threshold
的值通常爲負載因子乘以容量大小.oldCap
(原table的大小).(完)
下一篇: 深刻理解HashMap(五): 關鍵源碼逐行分析之put
查看更多系列文章:系列文章目錄