HashMap結構和原理

1.HashMap的底層結構?

HashMap是我們⾮常常⽤的數據結構,由數組和鏈表組合構成的數據結構。
⼤概如下,數組⾥⾯每個地⽅都存了Key-Value這樣的實例,在Java7叫Entry在Java8中叫Node。
在這裏插入圖片描述
因爲他本身所有的位置都爲null,在put插⼊的時候會根據key的hash去計算⼀個index值。
就⽐如我put(”帥丙“,520),我插⼊了爲”帥丙“的元素,這個時候我們會通過哈希函數計算出插⼊的位置,計算出來index是2那結果如下。
hash(“帥丙”)= 2
在這裏插入圖片描述

2.爲啥需要鏈表,鏈表⼜是怎麼樣⼦的呢?

    我們都知道數組⻓度是有限的,在有限的⻓度⾥⾯我們使⽤哈希,哈希本身就存在概率性,就是”帥丙“和”丙帥“我們都去hash有⼀定的概率會⼀樣,就像上⾯的情況我再次哈希”丙帥“極端情況也會hash到⼀個值上,那就形成了鏈表。
在這裏插入圖片描述
每⼀個節點都會保存⾃身的hash、key、value、以及下個節點,我看看Node的源碼。
在這裏插入圖片描述

3.新的Entry節點在插⼊鏈表的時候,是怎麼插⼊的麼?

    java8之前是頭插法,就是說新來的值會取代原有的值,原有的值就順推到鏈表中去,就像上⾯的例⼦⼀樣,因爲寫這個代碼的作者認爲後來的值被查找的可能性更⼤⼀點,提升查找的效率。
    在java8之後,都是所⽤尾部插⼊了。

4.頭插法爲啥要改尾插?

首先,瞭解一下HashMap擴容機制:

數組容量是有限的,數據多次插⼊的,到達⼀定的數量就會進⾏擴容,也就是resize。

4.1 什麼時候resize呢?
有兩個因素:
Capacity: HashMap當前⻓度。
LoadFactor: 負載因⼦,默認值0.75f
    怎麼理解呢,就⽐如當前的容量⼤⼩爲100,當你存進第76個的時候,判斷髮現需要進⾏resize了,那就進⾏擴容,但是HashMap的擴容也不是簡單的擴⼤點容量這麼簡單的
4.2 擴容?它是怎麼擴容的呢?
分爲兩步
擴容創建⼀個新的Entry空數組,⻓度是原數組的2倍。
ReHash:遍歷原Entry數組,把所有的Entry重新Hash到新數組。
4.3爲什麼要重新Hash呢,直接複製過去不行麼?
是因爲⻓度擴⼤以後,Hash的規則也隨之改變。
Hash的公式—> index = HashCode(Key) & (Length - 1)
原來⻓度(Length)是8你位運算出來的值是2 ,新的⻓度是16你位運算出來的值明顯不⼀樣了。
擴容前:
在這裏插入圖片描述
擴容後:
在這裏插入圖片描述
        我先舉個例⼦吧,我們現在往⼀個容量⼤⼩爲2的put兩個值,負載因⼦是0.75是不是我們在put第⼆個的時候就會進⾏resize?
2*0.75 = 1 所以插⼊第⼆個就要resize了
現在我們要在容量爲2的容器⾥⾯⽤不同線程插⼊A,B,C,假如我們在resize之前打個短點,那意味着數據都插⼊了但是還沒resize那擴容前可能是這樣的。
我們可以看到鏈表的指向A->B->C
Tip:A的下⼀個指針是指向B的
在這裏插入圖片描述
因爲resize的賦值⽅式,也就是使⽤了單鏈表的頭插⼊⽅式,同⼀位置上新元素總會被放在鏈表的頭部位置,在舊數組中同⼀條Entry鏈上的元素,通過重新計算索引位置後,有可能被放到了新數組的不同位置上。
就可能出現下⾯的情況,⼤家發現問題沒有?
B的下⼀個指針指向了A
在這裏插入圖片描述
⼀旦⼏個線程都調整完成,就可能出現環形鏈表
在這裏插入圖片描述
如果這個時候去取值,悲劇就出現了——Infinite Loop。
尾插法
    因爲java8之後鏈表有紅⿊樹的部分,⼤家可以看到代碼已經多了很多if else的邏輯判斷了,紅⿊樹的引⼊巧妙的將原本O(n)的時間複雜度降低到O(logn)
使⽤頭插會改變鏈表的上的順序,但是如果使⽤尾插,在擴容時會保持鏈表元素原本的順序,就不會出現鏈表成環的問題了
總結:

     Java7在多線程操作HashMap時可能引起死循環,原因是擴容轉移後前後鏈表順序倒置,在轉移過程中修改了原來鏈表中節點的引⽤關係。
     Java8在同樣的前提下並不會引起死循環,原因是擴容轉移後前後鏈表順序不變,保持之前節點的引⽤關係。

5.java8就可以把HashMap⽤在多線程中呢?

    我認爲即使不會出現死循環,但是通過源碼看到put/get⽅法都沒有加同步鎖,多線程情況最容易出現的就是:⽆法保證上⼀秒put的值,下⼀秒get的時候還是原值,所以線程安全還是⽆法保證。

6.HashMap的默認初始化⻓度是多少?

初始化⼤⼩是16
6.1爲啥是16?
    位與運算⽐算數計算的效率⾼了很多,之所以選擇16,是爲了服務將Key映射到index的算法。
6.2爲啥我們重寫equals⽅法的時候需要重寫hashCode⽅法呢?
因爲在java中,所有的對象都是繼承於Object類。Ojbect類中有兩個⽅法equals、hashCode,這兩個⽅法都是⽤來⽐較兩個對象是否相等的。
在未重寫equals⽅法我們是繼承了object的equals⽅法,那⾥的 equals是⽐較兩個對象的內存地址,顯然我們new了2個對象內存地址肯定不⼀樣

對於值對象,==⽐較的是兩個對象的值
對於引⽤對象,⽐較的是兩個對象的地址

⼤家是否還記得我說的HashMap是通過key的hashCode去尋找index的,那index⼀樣就形成鏈表了,也就是說”帥丙“和”丙帥“的index都可能是2,在⼀個鏈表上的。
我們去get的時候,他就是根據key去hash然後計算出index,找到了2,那我怎麼找到具體的”帥丙“還 是”丙帥“呢?
equals!是的,所以如果我們對equals⽅法進⾏了重寫,建議⼀定要對hashCode⽅法重寫,以保證相同的對象返回相同的hash值,不同的對象返回不同的hash值。

7.HashMap常⻅⾯試題:

1.HashMap的底層數據結構?
1.7的時候底層entry數組加鏈表,1.8的時候改成Node節點、鏈表和紅黑樹,
鏈表長度超過8,自動轉成紅黑樹,當紅黑樹的節點的個數小於等於6時,
會將紅黑樹結構轉爲鏈表結構
紅黑樹主要爲了解決查詢效率問題,但是需要設計左右節點的移動,
增刪會比較慢,不採用二叉樹的原因,是因爲樹高,樹越高查詢效率越慢
2.HashMap的存取原理?
存的時候:
根據key採用hash算法計算得到數組的索引位置上,1.7採用頭插法併發可能會導致擴容時,改變鏈表的順序,造成死循環,1.8採用尾插法,會保證鏈表的順序。
取的的時候:
根據key得到數組的索引,然後用equals去比較
3.Java7和Java8的區別?
差別:底層結構不一樣
	  1.7頭插,1.8尾插
4.爲啥會線程不安全?
併發存取時,⽆法保證上⼀秒put的值,下⼀秒get的時候還是原值
5.有什麼線程安全的類代替麼?
HashTable,ConcurrentHashMap
6.默認初始化⼤⼩是多少?爲啥是這麼多?爲啥⼤⼩都是2的冪?
初始化大小是16,採用位運算,位運算比算術效率高,
只要是2的冪就可以,就是爲了哈希的時候分佈更加均勻,減少哈希衝突
7.HashMap的擴容⽅式?負載因⼦是多少?爲什是這麼多?
擴容的時候,主要和當前hash表的長度和負載因子有關,
`當長度超過(當前hash表長度* 負載因子)`,會觸發擴容,
擴容的時候會創建一個空的entry數組,長度是當前長度的2倍,遍歷原來的entry數組,
重新hash到新的數組,不採用直接複製主要是因爲hash表的長度發生改變,
hash規則也發生了改變,負載因子是0.75,如果是0.5,數組的長度只有一半會被利用,
就會導致哈希衝突變多,影響查詢效率,如果是1的話,雖然能減少哈希衝突,
但是佔用的空間大了,所以採用0.75是基於空間和時間的綜合考慮
8.HashMap的主要參數都有哪些?

默認初始化容量默認初始化容量,必須是2的次方

/**
 * The default initial capacity - MUST be a power of two.
 * 默認初始化容量,必須是2的次方
 */
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

     默認初始化容量。這個是在採用無參構造方法實例化HashMap的時候,默認使用16作爲初始化容量。在第一次put的時候使用16來創建數組。

最大容量:

/**
 * The maximum capacity, used if a higher value is implicitly specified
 * by either of the constructors with arguments.
 * MUST be a power of two <= 1<<30.
 * 最大容量。即HashMap的數組容量必須小於等於 1 << 30
 */
static final int MAXIMUM_CAPACITY = 1 << 30;

最大容量。但必須是2的次方數,我們來看下面這段代碼:

public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        //如果初始化容量大於最大容量,則僅初始化爲最大容量。
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

可以看到,在指定容量的進行初始化時,會先判斷這個初始化容量是否大於最大容量,超過則使用最大容量來進行初始化。
默認負載因子

/**
 * The load factor used when none specified in constructor.
 * 默認的負載因子
 */
static final float DEFAULT_LOAD_FACTOR = 0.75f;

默認負載因子值爲0.75,但是可以通過構造方法來指定。一般是不建議修改的。
樹形化閾值

/**
 * The bin count threshold for using a tree rather than list for a
 * bin.  Bins are converted to trees when adding an element to a
 * bin with at least this many nodes. The value must be greater
 * than 2 and should be at least 8 to mesh with assumptions in
 * tree removal about conversion back to plain bins upon
 * shrinkage.
 */
static final int TREEIFY_THRESHOLD = 8;

即當鏈表的長度大於8的時候,會將鏈表轉爲紅黑樹,優化查詢效率。鏈表查詢的時間複雜度爲o(n) , 紅黑樹查詢的時間複雜度爲 o(log n )
解樹形化閾值

/**
 * The bin count threshold for untreeifying a (split) bin during a
 * resize operation. Should be less than TREEIFY_THRESHOLD, and at
 * most 6 to mesh with shrinkage detection under removal.
 */
static final int UNTREEIFY_THRESHOLD = 6;

當紅黑樹的節點的個數小於等於6時,會將紅黑樹結構轉爲鏈表結構。
樹形化的最小容量

/**
 * The smallest table capacity for which bins may be treeified.
 * (Otherwise the table is resized if too many nodes in a bin.)
 * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
 * between resizing and treeification thresholds.
 */
static final int MIN_TREEIFY_CAPACITY = 64;

前面我們看到有一個樹形化閾值,就是當鏈表的長度大於8的時候,會從鏈表轉爲紅黑樹。其實不一定是這樣的。轉爲紅黑樹有兩個條件:

① 鏈表的長度大於8
② HashMap數組的容量大於等於64

需要當上述兩個條件都成立的情況下,鏈表結構纔會轉爲紅黑樹

9.HashMap是怎麼處理hash碰撞的?
HashMap使用鏈表來解決碰撞問題,當碰撞發生了,對象將會存儲在鏈表的下一個節點中。
hashMap在每個鏈表節點存儲鍵值對對象。當兩個不同的鍵卻有相同的hashCode時,
他們會存儲在同一個bucket位置的鏈表中。鍵對象的equals()來找到鍵值對。
10.hash的計算規則?
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章