深刻理解HashMap(四): 關鍵源碼逐行分析之resize擴容

前言

系列文章目錄java

上一篇咱們說明了HashMap的構造函數, 談到構造函數中並不會初始化table 變量, table 變量是在 resize過程當中初始化的.node

本篇咱們就來聊聊HashMap的擴容: resizesegmentfault

本文的源碼基於 jdk8 版本.數組

resize

resize用於如下兩種狀況之一框架

  • 初始化table
  • 在table大小超過threshold以後進行擴容

下面咱們直接來對照源碼分析:函數

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;
}

resize時的鏈表拆分

下面咱們單獨來看看這段設計的很精妙的代碼工具

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鏈表, loHeadloTail 分別指向 lo鏈表的頭節點和尾節點, hiHeadhiTail以此類推.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 塊, 則上面這段的目的就很清晰了:

咱們首先準備了兩個鏈表 lohi, 而後咱們順序遍歷該存儲桶上的鏈表的每一個節點, 若是 (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

爲了幫助你們理解,我畫了個示意圖:
rezise
(ps: 畫個圖真的好累啊, 你們有什麼好的畫圖工具推薦嗎?)

關於 (e.hash & oldCap) == 0 j 以及 j+oldCap

上面咱們已經弄懂了鏈表拆分的代碼, 可是這個拆分條件看上去很奇怪, 這裏咱們來稍微解釋一下:

首先咱們要明確三點:

  1. oldCap必定是2的整數次冪, 這裏假設是2^m
  2. newCap是oldCap的兩倍, 則會是2^(m+1)
  3. hash對數組大小取模(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是無比佩服源碼做者的!

總結

  1. resize發生在table初始化, 或者table中的節點數超過threshold值的時候, threshold的值通常爲負載因子乘以容量大小.
  2. 每次擴容都會新建一個table, 新建的table的大小爲原大小的2倍.
  3. 擴容時,會將原table中的節點re-hash到新的table中, 但節點在新舊table中的位置存在必定聯繫: 要麼下標相同, 要麼相差一個oldCap(原table的大小).

(完)

下一篇: 深刻理解HashMap(五): 關鍵源碼逐行分析之put

查看更多系列文章:系列文章目錄

相關文章
相關標籤/搜索