HashMap淺析(二)

HashMap淺析(二)

擴容機制

首先上擴容的代碼,每一行都寫了註釋,儘可能的讓大家明白這個過程。

final Node<K,V>[] resize() {
    	//將table賦值給oldTab
        Node<K,V>[] oldTab = table;
    	//聲明oldTab的容量給oldCap
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
    	//擴容閾值賦值給oldThr
        int oldThr = threshold;
    	//上面這三行代碼,其實就是爲了區分出擴容前與擴容後的變量,衆所周知,數組無法動態擴容
    	//聲明新數組的容量和擴容閾值,暫時爲0
        int newCap, newThr = 0;
    	//如果舊數組的容量大於0
        if (oldCap > 0) {
            //如果舊數組的容量大於等於最大值,即1<<30
            if (oldCap >= MAXIMUM_CAPACITY) {
                //全局變量的擴容閾值賦值爲int的最大值,同時返回舊數組
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            //如果新數組的容量(舊數組容量*2)小於最大容量,且,舊數組容量大於等於默認值1<<4
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                //新數組的擴容閾值等於舊數組擴容閾值的2倍
                newThr = oldThr << 1; // double threshold
        }
    	//如果舊數組的擴容閾值大於0
        else if (oldThr > 0) // initial capacity was placed in threshold
            //新數組的擴容閾值就等於舊數組的擴容閾值
            newCap = oldThr;
    	//如果既不走oldCap>0,也不走oldThr>0的分支,那麼走else
        else {               // zero initial threshold signifies using defaults
            //新數組的容量爲默認容量:16
            newCap = DEFAULT_INITIAL_CAPACITY;
            //新數組的擴容閾值=默認載荷因素*默認初始容量=12
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
    	//新的if分支:如果新數組的擴容閾值等於0
        if (newThr == 0) {
            //ft=新數組容量*0.75,loadFactory在用戶未指定時,就是默認的0.75
            float ft = (float)newCap * loadFactor;
            //當新數組的容量小於最大容量,且ft小於最大容量時,新數組的擴容閾值就爲ft,
            //否則爲int的最大值
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
    	//將新得到的新數組的擴容閾值,賦值給全局變量的擴容閾值
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
    	//創建一個長度爲newCap的node數組
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    	//並讓全局變量的table數組指向新數組
        table = newTab;
    	//如果舊數組不等於空
        if (oldTab != null) {
            //循環舊數組
            for (int j = 0; j < oldCap; ++j) {
                //聲明一個Node對象,用來承載舊值
                Node<K,V> e;
                //將數組的j角標位置的值賦值給e,同時判斷是否爲null
                if ((e = oldTab[j]) != null) {
                    //不爲空,則把原數組的j位置重新賦值爲null
                    //當循環完畢,舊數組就是空數組,方便回收
                    oldTab[j] = null;
                    //判斷當前數組元素e的鏈表的下一個結點是否爲null
                    if (e.next == null)
                        //證明當前數組元素只有一個結點,所以只需在新數組中放置e元素
                        newTab[e.hash & (newCap - 1)] = e;
                    //首先走到else if分支這裏,已經確定e.next不爲null
                    //那麼就需要判斷是紅黑樹還是鏈表
                    else if (e instanceof TreeNode)
                        //紅黑樹部分先過掉
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    //確定爲鏈表之後
                    else { // preserve order
                        //創建五個node對象
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        //注意是do-while循環,即先執行一次,再走while
                        do {
                            //next即爲當前遍歷到的元素的下一個結點
                            next = e.next;
                            //如果當前元素的hash&舊數組容量的值爲0
                            //當你看到這裏時,請先看下一節:擴容中鏈表複製的詳解
                            //便於理解之後的流程
                            //判斷角標是否需要修改,下面的分析會單獨拿出來,因爲涉及到循環和判斷
                            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;
    }

do-while中具體執行流程

現在假設當前元素e的鏈表中存在三個結點,a、b、c分別爲三個結點的內存地址,假設e不需要重新計算角標

也就是e ->a, e.next->b ,e.next.next->c

在這裏插入圖片描述

第一次執行

 						Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            //第一次執行,next->b,e->a
                            next = e.next;
                            //判斷元素位置是否改變
                            if ((e.hash & oldCap) == 0) {
                                //不需要改變走這個分支
                                //判斷loTail是否爲null,現在是第一次執行,肯定是null,進入if分支
                                if (loTail == null)
                                    //loHead->a
                                    loHead = e;
                                else
                                    loTail.next = e;
                                //loTail->a
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                            //然後執行判斷的同時,將e->next,e->b
                        } while ((e = next) != null);

用個圖表示第一次執行之後,變量的關係

在這裏插入圖片描述

第二次循環

 						Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            //next->c,e->b
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                //loTail->a,所以走else
                                if (loTail == null)
                                    loHead = e;
                                else
                                    //loTail.next->b
                                    loTail.next = e;
                                //loTail->b
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                            //然後執行判斷的同時,將e->next,e->c
                        } while ((e = next) != null);

在這裏插入圖片描述

第三次循環

 						Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            //next->null,e.next->null,e->c
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    //loTail.next->c
                                    loTail.next = e;
                                //loTail->c
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                            //e=next == null,while循環結束
                        } while ((e = next) != null);

在這裏插入圖片描述

跳出循環之後的處理

						//loTail不爲null,因爲loTail指向的是a的內存地址,而a又是鏈表的頭結點
						//所以只需要將新數組的j位置元素指向loHead的地址,最終也就是指向的a
						if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
						//這個方法是針對需要重新計算元素下角標的元素
                        if (hiTail != null) {
                            hiTail.next = null;
                            //針對爲何是j+oldCap可以查看下面的內容
                            newTab[j + oldCap] = hiHead;
                        }

擴容中鏈表複製的詳解

在resize()方法中,針對鏈表的複製,HashMap採取了一個非常有意思的方法。

它沒有針對新數組重新計算hash,從而確定下角標,而是通過e.hash & oldCap判斷元素的前後位置是否有變化。

  1. 假設當前元素的hash爲10:1010(二進制),數組擴容前爲8:1000(二進制),

    那麼計算角標的方法就是(8-1)&10,index=2

    在這裏插入圖片描述

  2. 擴容後的下角標,hash爲10:1010(二進制),數組擴容後爲16:0001 0000(二進制),

    那麼計算角標的方法就是(16-1)&10,index=10

    在這裏插入圖片描述

大家仔細看,其實擴容就是高一位從0到1,這時如果確定hash的對應位是0還是1,就可以確定,元素在新數組的位置是否有變化了。

在這裏插入圖片描述

那麼如何確定呢,e.hash & oldCap就是用來判斷這個變化的。

首先&的運算,就保證瞭如果兩個二進制的對應位都是1,計算出來的值的對應位才爲1.

在這裏插入圖片描述

由上圖可知,e.hash & oldCap!=0,所以需要重新計算位置。

通過下圖,你能發現什麼問題呢?

新舊座標的差值其實就是數組擴容前後,區分出來的高一位1,然後又因爲數組擴容是嚴格按照2的冪次方進行的,所以必然存在:

當e.hash & oldCap!=0時,

新數組的下角標位置=舊數組的下角標+2的m次方(m=圖中容量擴容前後-1的二進制碼中的發生變化的位置)
newIndex=oldIndex+2m newIndex=oldIndex+2^m

2m=oldCap 2^m=oldCap
所以有
newIndex=oldIndex+oldCap newIndex=oldIndex+oldCap

在這裏插入圖片描述

如果還不夠明白的話,我繼續通過推導e.hash & oldCap==0的情況給各位再反向操作一波。

爲了方便推導,我將其中的hash值修改了一下,下圖是擴容前後求index的圖。

下圖可知:index值在擴容前後沒有發生變化。

在這裏插入圖片描述

下圖是e.hash & oldCap的計算圖,對應圖中標註的1.2.3,依次做一個解釋。

1.因爲容量是嚴格按照2的冪次方擴容,所以整段二進制碼中,只存在一個1;

2.兩者進行"&"計算後,如果hash的對應位(指容量的二進制碼中1的位置)爲0,那麼結果必然爲0.

3.反之亦然。

當擴容後的容量爲16, 0001 0000,

所對應的n-1,即16-1, 0000 [1]111,

舊容量的n-1,即8-1, 0000 [0]111,

二者的區別就在[]號標識的那一位,所以利用上面2.的結論,無論是擴容多少次,滿足e.hash & oldCap==0的條件下,角標都不會變

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章