HashMap源碼解析 (HashMap類-resize方法)

擴容方法 resize( )

擴容機制:

  1. 什么時候才需要擴容
    HashMap 中的元素個數(shù)超過數(shù)組大小(數(shù)組長度)*loadFactor(負載因子)時,就會進行數(shù)組擴容,loadFactor 的默認值是 0.75
  2. HashMap 的擴容是什么
    進行擴容,會伴隨著一次重· hash 分配,并且會遍歷 hash 表中所有的元素,是非常耗時的。在編寫程序中,要盡量避免resize
    設(shè)計計算hash值時,要避免出現(xiàn)hash沖突。也就是說不能讓它出現(xiàn)鏈表,更不應(yīng)該出現(xiàn)紅黑樹,這樣性能很差,如果出現(xiàn)了,證明hash算法,設(shè)計的太差。為了避免hash沖突,一,hash算法,二,加載因子
    HashMap 在進行擴容時,使用的 rehash(偽哈希) 方式非常巧妙,因為每次擴容都是翻倍,與原來計算的 (n - 1) & hash 的結(jié)果相比,只是多了一個 bit位,所以結(jié)點要么就在原來的位置,要么就被分配到 “原位置 + 舊容量” 這個位置
源數(shù)組長度:  16   n = 16  n-1=15
(n-1) & hash

                    0000 0000 0000 0000 0000 0000 0001 0000   16
                    0000 0000 0000 0000 0000 0000 0000 1111   15  n-1
假設(shè)一個hash1(key1): 1111 1111 1111 1111 0000 1111 0000 0101
-----------------------------------------------------------------------------------
                    0000 0000 0000 0000 0000 0000 0000 0101   索引 5
                    
                    
                    0000 0000 0000 0000 0000 0000 0000 1111   15  n-1
假設(shè)一個hash2(key2): 1111 1111 1111 1111 0000 1111 0001 0101            
-----------------------------------------------------------------------------------
                    0000 0000 0000 0000 0000 0000 0000 0101   索引 5

=======================================================================================

此時數(shù)組擴容--> 16 * 2 = 32   n = 32  n-1=31
(n-1) & hash

                    0000 0000 0000 0000 0000 0000 0010 0000   32
                    0000 0000 0000 0000 0000 0000 0001 1111   31  n-1
假設(shè)一個hash1(key1): 1111 1111 1111 1111 0000 1111 0000 0101
-----------------------------------------------------------------------------------
                    0000 0000 0000 0000 0000 0000 0000 0101   索引 5
                    
                    0000 0000 0000 0000 0000 0000 0001 1111   31  n-1
假設(shè)一個hash2(key2): 1111 1111 1111 1111 0000 1111 0001 0101            
-----------------------------------------------------------------------------------
                    0000 0000 0000 0000 0000 0000 0001 0101   索引 5 + 16
                    
擴容之后的索引位置要么是原來索引,要么是原來索引 + 舊數(shù)組容量
舉例:
5 或者 5 + 16

因此元素在重新計算hash之后,因為n變?yōu)?2 倍,那么 n - 1 的標記范圍在高位多 1bit(紅色),因此新的index就會發(fā)生這樣的變化


5 是假設(shè)計算出來的原來的索引。這樣就驗證了上述所描述的:擴容之后所以節(jié)點要么就在原來的位置,要么就被分配到 “原位置 + 舊容量” 這個位置
因此,我們在擴充HashMap的時候,不需要重新計算 hash,只需要看看原來的hash 值新增的那個 bit 是 1 還是 0 就可以了,是 0 的話索引沒變,是 1 的話索引變成 “原位置 + 舊容量” ??梢钥纯聪聢D為 16 擴充為 32 的resize示意圖:

正是因為這樣巧妙的 rehash(偽哈希) 方式,既省去了重新計算hash值的時間,而且同時,由于新增的1bit 是 0 還是 1 可以認為是隨機的,在 resize的過程中保證了rehash(偽哈希)之后每個桶上的結(jié)點數(shù)一定小于等于原來桶上的結(jié)點數(shù),保證了 rehash(偽哈希) 之后不會出現(xiàn)更嚴重的 hash沖突,均勻的把之前的沖突的結(jié)點分散到新的桶中了

源碼 resize 方法的解讀

final Node<K,V>[] resize() {
    // 得到當前數(shù)組
    Node<K,V>[] oldTab = table;
    // 如果當前數(shù)組等于null長度返回0,否則返回當前數(shù)組的長度
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //當前閥值點 默認是12(16*0.75)
    int oldThr = threshold;
    int newCap, newThr = 0;
    // 如果老的數(shù)組長度大于0
    // 開始計算擴容后的大小
    if (oldCap > 0) {
        // 超過最大值就不再擴充了,就只好隨你碰撞去吧
        if (oldCap >= MAXIMUM_CAPACITY) {
            // 修改閾值為int的最大值
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        /*
            沒超過最大值,就擴充為原來的2倍
            1) (newCap = oldCap << 1) < MAXIMUM_CAPACITY 擴大到2倍之后容量要小于最大容量
            2)oldCap >= DEFAULT_INITIAL_CAPACITY 原數(shù)組長度大于等于數(shù)組初始化長度16
        */
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 閾值擴大一倍
            newThr = oldThr << 1; // double threshold
    }
    // 老閾值點大于0 直接賦值
    else if (oldThr > 0) // 老閾值賦值給新的數(shù)組長度
        newCap = oldThr;
    else { // 直接使用默認值
        newCap = DEFAULT_INITIAL_CAPACITY;//16
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 計算新的resize最大上限
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    // 新的閥值 默認原來是12 乘以2之后變?yōu)?4
    threshold = newThr;
    // 創(chuàng)建新的哈希表
    @SuppressWarnings({"rawtypes","unchecked"})
    //newCap是新的數(shù)組長度--》32
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    // 判斷舊數(shù)組是否等于空
    if (oldTab != null) {
        // 把每個bucket都移動到新的buckets中
        // 遍歷舊的哈希表的每個桶,重新計算桶里元素的新位置
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                // 原來的數(shù)據(jù)賦值為null 便于GC回收
                oldTab[j] = null;
                // 判斷數(shù)組是否有下一個引用
                if (e.next == null)
                    // 沒有下一個引用,說明不是鏈表,當前桶上只有一個鍵值對,直接插入
                    newTab[e.hash & (newCap - 1)] = e;
                //判斷是否是紅黑樹
                else if (e instanceof TreeNode)
                    // 說明是紅黑樹來處理沖突的,則調(diào)用相關(guān)方法把樹分開
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // 采用鏈表處理沖突
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    // 通過上述講解的原理來計算結(jié)點的新位置
                    do {
                        // 原索引
                        next = e.next;
                         // 這里來判斷如果等于true e這個結(jié)點在resize之后不需要移動位置
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        // 原索引+oldCap
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    // 原索引放到bucket里
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    // 原索引+oldCap放到bucket里
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 擴容(resize)就是重新計算容量,向HashMap對象里不停的添加元素,而HashMap對象內(nèi)部的數(shù)組無法裝載...
    luke_閱讀 3,097評論 1 2
  • HashMap底層原理解析 1.基本、常用性質(zhì)HashMap儲存的是鍵值對HashMap 允許 null 鍵和 n...
    瀟蕭之炎閱讀 701評論 0 1
  • 簡介 Java為數(shù)據(jù)結(jié)構(gòu)中的映射定義了一個接口java.util.Map,此接口主要有四個常用的實現(xiàn)類,分別是Ha...
    bbe9e62bc5ba閱讀 455評論 0 4
  • hashMap發(fā)生死鎖的問題,在以下文章中:1.7hashmap發(fā)生死鎖,線程非安全[https://blog.c...
    zhengaoly閱讀 225評論 0 0
  • 摘要 HashMap是程序猿使用頻率最高的用于映射(鍵值對)的數(shù)據(jù)類型。JDK1.8對HashMap進行了優(yōu)化,例...
    一凡呀閱讀 756評論 0 2

友情鏈接更多精彩內(nèi)容