1. 程式人生 > >【資料結構】8.java原始碼關於HashMap

【資料結構】8.java原始碼關於HashMap

1.hashmap的底層資料結構

眾所皆知map的底層結構是類似鄰接表的結構,但是進入1.8之後,連結串列模式再一定情況下又會轉換為紅黑樹
在JDK8中,當連結串列長度達到8,並且hash桶容量超過64(MIN_TREEIFY_CAPACITY),會轉化成紅黑樹,以提升它的查詢、插入效率底層雜湊桶的資料結構是陣列,所以也會涉及到擴容的問題。
當MyHashMap的容量達到threshold域值時,就會觸發擴容。擴容前後,雜湊桶的長度一定會是2的次方。

1.1 為什麼用紅黑樹

那麼為什麼用紅黑樹呢?之前都是用的連結串列,之前的文章有提到連結串列的隨機訪問效率是很低的,因為需要從head一個個往後面找,那麼時間複雜度就是O(n),但是如果是紅黑樹因為紅黑樹是平衡二叉樹,說白了就是可以索引的,那麼時間複雜度只有O(logn),這樣效率就可以得到很大的提高
也許有人就想問了,那為什麼還搞個連結串列啊,直接用紅黑樹不就完了:
1.連結串列比紅黑樹簡單,構造一個紅黑樹要比構造連結串列複雜多了,所以在連結串列不多的情況下,整體效能上來看,當連結串列不長的時候紅黑樹的效能不一定有連結串列高
2.還有一個節點的新增和刪除的時候,需要對紅黑樹進行旋轉,著色等操作,這個就比連結串列的操作複雜多了
3.所以為連結串列設定一個閾值用來界定什麼時候進行樹化,什麼時候維持連結串列,從中間取得一個均衡是很重要的

1.2 為什麼閾值是64,連結串列長度到8

剛剛講到紅黑樹查詢效率是O(logn)那麼8的log是3,而使用連結串列,我們之前也有提到,原始碼會進行折半查詢(參考之前linkedlist原始碼分析)那就是8/2 = 4 平均查詢長度是4,所以在8的時候是比較合適的因為3比4小
再比如連結串列長度為6的時候,紅黑樹會退化為連結串列同理:6=》log=2~3 和8類似,但是6/2=3也很快,而且紅黑樹很複雜,所以是用的連結串列,至於其中的數字7的作用是緩衝一下,避免再長度為7,8徘徊的時候會頻繁修改為紅黑樹和連結串列
還有為什麼是64,參考網上記錄是:再低於64的時候容量比較小,hash碰撞的機率比較大,這種時候出現長連結串列的可能性比較大,這種原因導致的長連結串列我們應該避免,而是採用擴容的策略避免不必要的樹化

 

接下來我們觀察一下hashmap的繼承結構,瞭解一下

 

1.3 還有個問題負載因子的作用

 

0.75f負載因子過高會導致連結串列過長,查詢鍵值對時間複雜度就會增高,負載因子過低會導致hash桶的個數過多,空間複雜度變高


注意建構函式:

hash桶沒有再建構函式中進行初始化,而是再第一次儲存鍵值的時候進行初始化,initialCapacity返回一個大於等於初始化容量大小的最小2的冪次方

 

 2.hashmap的增長策略

 2.1 插入資料

 

1.插入資料的時候首先會判斷hash桶是否為空,如果為空會進行初始化,這是避免呼叫建構函式之後沒有資料導致,而且再初始化的時候會呼叫擴容策略這個後面再講
通過剛剛的學習我們知道hashmap有三種資料存放模式:陣列,連結串列,紅黑樹
判斷是否為空,如果為空,直接陣列存放
這裡有個細節

hash(key)和(n - 1) & hash 的使用
第一個對key進行hash取值

 2.1.1 為什麼要用hash(key),當然hash肯定是必須的,不然object物件怎麼定位陣列索引但是hashcode不行麼?

 

這裡是因為hashcode是32位的資料,用hashcode和n相與的時候,如果n比較小,那麼高位的資料基本就沒用到(2的16次冪以上的資料),那麼就會導致hash碰撞的概率加大
這裡hash(key)的操作是吧hashcode右移16位在和原來的hashcode進行異或操作,相當於是吧高位的資訊合併到低位上,然後在和n做與運算,這樣高位低位的資訊全部都有,綜合的話hash碰撞的概率相應減低

 

 2.1.2 (n-1)&hash是什麼操作hash%n不行麼?

 

------------------------------------------------------------------------------------------------------------------------------------
說明一下,這兩個操作都是取餘操作,之前有人說是取模,這裡科普一下,取模和取餘是不一樣的
取模(百度百科):取模運算(“Module Operation”)和取餘運算(“Complementation ”)兩個概念有重疊的部分但又不完全一致。主要的區別在於對負整數進行除法運算時操作不同。取模主要是用於計算機術語中。取餘則更多是數學概念。模運算在數論和程式設計中都有著廣泛的應用,從奇偶數的判別到素數的判別,從模冪運算到最大公約數的求法,從孫子問題到凱撒密碼問題,無不充斥著模運算的身影。雖然很多數論教材上對模運算都有一定的介紹,但多數都是以純理論為主,對於模運算在程式設計中的應用涉及不多。
7 mod 4 = 3(商 = 1 或 2,1<2,取商=1)
-7 mod 4 = 1(商 = -1 或 -2,-2<-1,取商=-2)
7 mod -4 = -1(商 = -1或-2,-2<-1,取商=-2)
-7 mod -4 = -3(商 = 1或2,1<2,取商=1)
R = a -c*b
比如-7 mod 4 => -7 = 1 -2 * 4
求模運算和求餘運算在第一步不同: 取餘運算在取c的值時,向0 方向舍入(fix()函式);而取模運算在計算c的值時,向負無窮方向舍入(floor()函式)。

符號相同時,兩者不會衝突。比如,7/3=2.3,產生了兩個商2和37=3*2+1或7=3*3+(-2)。因此,7rem3=1,7mod3=1。符號不同時,兩者會產生衝突。比如,7/(-3)=-2.3,產生了兩個商-2和-37=(-3)*(-2)+1或7=(-3)*(-3)+(-2)。因此,7rem(-3)=1,7mod(-3)=(-2)

------------------------------------------------------------------------------------------------------------------------------------

好的,我們繼續討論(n-1)&hash和hash%n的問題

之前也有說到hashmap的擴容策略是大於等於初始化容量大小的最小2的冪次方,那麼也就是說n是2的倍數,轉換成2進位制也就是最低位是0,再進行-1,那就是奇數
而且進行&操作

這裡注意我們的n是2的多次冪,那麼就是000100000000類似這樣的二進位制,減一的結果就是除了最高位其餘一下都是1也就是:000011111111111
這個時候和原來的資料hash做&操作,就會把超出這個length範圍的資料全部設定為0,也就是這個範圍以內的資料不會變

Example:

8 =》 0000 0000 0000 1000
8 - 1 =》 0000 0000 0000 0111
然後不論什麼資料與8-1做&操作,那麼範圍都在 0111之內,也就是7以內包含7範圍再0~7,這樣懂了吧,比如1000000&(7-1)結果就是0~7
當然出現這種情況有個必要的條件就是長度必須是2的n次冪,這樣再二進位制數列中,永遠只有一個位置是1,其餘位置是0,-1之後,這個位置一下的資料全包含再裡面&就是擷取低位的資料,吧高位去掉,相當於是取餘了
因為不論什麼數字都是x = a1*2^(n-1) + a2*2^(n-2) + … + a(n-1)*2^(1) + a(n)*2^(0),高位的肯定都是2的y次冪的倍數,所以去掉倍數,剩下的就是餘數,不知道我這麼說大家有沒有理解。。。
大家還可以看看我之前的部落格:https://www.cnblogs.com/cutter-point/p/11091727.html

如果不為空那麼就要進行連結串列化或者樹化了

 2.1.3 如何連結串列化

 說白了就是再hash桶的陣列上獲取這個位置上的node節點,然後迴圈遍歷獲取到最後一個節點,然後插入到節點末尾

//連結串列存放
for (int binCount = 0; ; ++binCount) {
    if ((e = p.next) == null) {
        //連結串列尾部插入,p的next判斷是否為空
        p.next = newNode(hash, key, value, null);
        //當連結串列的長度大於等於樹化閥值,並且hash桶的長度大於等於MIN_TREEIFY_CAPACITY,連結串列轉化為紅黑樹
//                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
//                            treeifyBin(tab, hash);
        break;
    }
    //連結串列中包含鍵值對
    if (e.hash == hash &&
            ((k = e.key) == key || (key != null && key.equals(k))))
        break;
    p = e;
}

 

2.1.4 構造紅黑樹樹化

紅黑樹的變換規則可以參考我之前的部落格:https://www.cnblogs.com/cutter-point/p/10976416.html

我們什麼時候會進行樹化呢???
就是當我們的連結串列長度超過或等於8個的時候

 

至於如何吧這個連結串列組建為紅黑樹,這個以後單獨開章節細細探討。。。。

2.2 擴容策略resize

 

//陣列擴容
public Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    //如果舊hash桶不為空
    if (oldCap > 0) {
        ////超過hash桶的最大長度,將閥值設為最大值
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        //新的hash桶的長度2被擴容沒有超過最大長度,將新容量閥值擴容為以前的2倍
        //擴大一倍之後,小於最大值,並且大於最小值
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                oldCap >= DEFAULT_INITIAL_CAPACITY)
            //左移1位,也就是擴大2倍
            newThr = oldThr << 1;
    }
    else if (oldThr > 0) //如果舊的容量為空,判斷閾值是否大於0,如果是那麼就把容量設定為當前閾值
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }

    //如果閾值還是0,重新計算閾值
    if (newThr == 0) {
        //當HashMap的資料大小>=容量*載入因子時,HashMap會將容量擴容
        float ft = (float)newCap * loadFactor;
        //如果容量還沒超MAXIMUM_CAPACITY的loadFactor時候,那麼就返回ft,否則就是反饋int的最大值
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                (int)ft : Integer.MAX_VALUE);
    }
    //hash桶的閾值
    threshold = newThr;
    //初始化hash桶
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;

    if (oldTab != null) {
        //遍歷舊陣列
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            //如果舊的hash桶不為空,需要將舊的hash表裡的鍵值對重新對映到新的hash桶中
            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 { // preserve order
                    //如果是多個節點的連結串列,將原連結串列拆分為兩個連結串列,兩個連結串列的索引位置,一個為原索引,一個為原索引加上舊Hash桶長度的偏移量
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
//                            在遍歷原hash桶時的一個連結串列時,因為擴容後長度為原hash表的2倍,假設把擴容後的hash表分為兩半,分為低位和高位,
//                            如果能把原連結串列的鍵值對, 一半放在低位,一半放在高位,這樣的索引效率是最高的
                        //這裡的方式是e.hash & oldCap,
                        //經過rehash之後,元素的位置要麼是在原位置,要麼是在原位置再移動2次冪的位置。對應的就是下方的resize的註釋
                        //為什麼是移動2次冪呢??注意我們計算位置的時候是hash&(length - 1) 那麼如果length * 2 相當於左移了一位
                        //也就是擷取的就高了一位,如果高了一位的那個二進位制正好為1,那麼結果也相當於加了2倍
                        //hash & (length * 2 - 1) = length & hash + (length - 1) & hash
                        if ((e.hash & oldCap) == 0) {
                            //如果這個為0,那麼就放到lotail連結串列
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            //如果length & hash 不為0,說明擴容之後位置不一樣了
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        //而這個loTail連結串列就放在原來的位置上
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        //因為擴容了2倍,那麼新位置就可以是原來的位置,右移一倍原始容量的大小
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

總結就是擴容的時候吧陣列大小擴大一倍,相當於左移1位,並且要重新計算hash雜湊值,找對應的位置填充
連結串列也要進行拆分,連結串列的拆分主要就體現在:
如果原來hash索引的位置就是這裡,那麼還是連線再原來的節點上,如果取餘到對應的位置的節點,陣列擴大一倍,我們原來的計算方式是hash&(n - 1)
那麼如果我們大小擴大一倍結果就是:hash&(2n - 1)=hash&n + hash&(n-1)因為n是2的n次冪,除了對應的位置為1其餘位置都為0
那麼這裡就可以轉換為hash&(2n - 1)=hash&n + hash&(n-1) => n + hash&(n-1) => oldIndex + oldCap 也就是舊索引位置加上舊的容量大小

 3.hashmap查詢資料

 

查詢對於紅黑樹部分我們略過:
至於其他部分,也就是跟之前大同小異了,還是hash取位置,然後取餘獲取對應的索引下標
首先檢查是不是第一個,如果是那就直接返回了
如果不是迴圈遍歷連結串列找到對應的key為止

 

final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    //注意這一步中(n - 1) & hash 的值 等同於 hash(k)%table.length
    if ((tab = table) != null && (n = tab.length) > 0 &&
            //這裡是計算相當於是取餘的索引位置(n - 1) & hash 等價於hash % n
            //而且由於hashmap中的length再tableSizeFor的時候,就把長度設定為2的n次冪了,那麼n-1之後的值,就是最高位全都是0,下面位數全是1
            //這個也就是取hash的低位的值
            (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            //暫時不考慮紅黑樹
//                if (first instanceof TreeNode)
//                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do {
                if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

 

 4.hashmap刪除資料

 

4.1 樹形退化
紅黑樹,我們就略過吧,這裡篇幅有限不做探討。。。。

 5.關於hashmap的特殊操作

 

這裡可以講講hashmap的特殊地方了
1.hashmap是允許null鍵和值的,而hashtable就不允許了

 

 


參考:
https://juejin.im/post/5a7719456fb9a0633e51ae14
https://blog.csdn.net/xingfei_work/article/details/79637878
https://juejin.im/post/5bed97616fb9a049b77fefbf
https://www.zhihu.com/question/30526656
https://juejin.im/post/5cb09c85e51d456e3428c0cf

&n