HashMap底層數據結構?
-
底層:數組+鏈表 大概結構如圖:
-
能說得再詳細一點嗎?
1.在jdk1.7中,HashMap的主干由一個一個的Entry數組組成,源碼:
/** * An empty table instance to share when the table is not inflated. */ static final Entry<?,?>[] EMPTY_TABLE = {}; /** * The table, resized as necessary. Length MUST Always be a power of two. */ transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
static class Entry<K,V> implements Map.Entry<K,V> { final K key; V value; Entry<K,V> next; int hash; /** * Creates new entry. */ Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; } ...... //后面的省略 }
2.jdk1.8中,HashMap主干由名叫Node的數組組成,源碼:
transient Node<K,V>[] table;
static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; V value; Node<K,V> next; Node(int hash, K key, V value, Node<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } .... //略 }
以及一些其他的默認屬性:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默認數組長度2^4=16 static final int MAXIMUM_CAPACITY = 1 << 30; // 最大數組容量2^30 static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默認負載因子 static final int TREEIFY_THRESHOLD = 8; // 鏈表轉紅黑樹的閾值 static final int UNTREEIFY_THRESHOLD = 6; // 擴容時紅黑樹轉鏈表的閾值
說說put方法的具體?
-
put的實現原理
在每次put數據的時候,會調用HashMap的hashCode計算key的hash值,然后執行:
n = tab.length //n就是數組的長度 index = (n - 1) & hash
計算出的index,就是HashMap數組的下標索引,然后添加進去,但是在添加的的時候還會做檢查:
1.檢查到這個位置沒有數據,則直接添加。
2.檢查到這個位置有數據,則還需要再做一次判斷:
(1)調用equals方法判斷這兩個key是不是一樣:
如果是同一個key,那么就對之前的value進行覆蓋,如果不同,則添加在后面,這樣就會形成鏈表。(這是jdk1.8以后的尾插法,之前都是在頭部插入,有什么區別呢?后續~~),
注:可能會有小伙伴會有疑問,為什么兩個不同的key值會計算出相同的索引??
這就是所謂的hash沖突,即便內容不同,但是很有可能計算出來的hashCode值一樣。
jdk1.7和jdk1.8put的區別是什么?
-
jdk1.7中使用的是 “頭部插入法”,即當有新的元素加入到鏈表中的時候,是加在鏈表頭部。然而,從jdk1.8以后,都修改了,執行“尾部插入法”,即插在鏈表的末尾。
-
為什么要把“頭部插入法”修改為“尾部插入法”呢?
也許有點小伙伴會覺得,這也沒啥講究的,可能人家心情不好就隨手給改了~~~~然鵝,這里面大有文章!!!!
我們知道,HashMap是會動態擴容的,擴容的影響因素有兩個:
- Capacity:HashMap當前長度。
- LoadFactor:負載因子,默認值0.75f。
- 官方源碼說明:
The next size value at which to resize (capacity * load factor).
如果不指定長度,一開始的capacity默認值是16(為什么是16呢?后續),當添加了:
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //即閾值 = 16*0.75=12
當添加了12個數據,在添加第13個的時候,就會進行擴容,而且擴容為原來的兩倍!!(擴容細節后續~~~)
-
這個擴容和尾插法有什么關系呢??
舉個例子,現在往一個容量大小為2的HashMap中put兩個值,負載因子是0.75,則在put第二個的時候就會進行resize!
現在我們用
多個線程
插入三個鍵值對:A = 1 ,B = 2,C = 3 ;如果我們在 resize 執行之前打一個斷點,這樣就會出現數據已經添加了但是還沒來得及擴容,此時 A 是指向 B 的:
而resize的機制是,使用
單鏈表的頭插入方法,同一位置上的新元素總會被放到鏈表的頭部
,而在擴容之前數組中同一條Entry鏈上的元素,會被重新計算,然后放到不同的位置上,如果這時候剛好把 B 放到了A原來的位置,如圖: 此時的情況是,B 居然指向了 A ???還記得沒有擴容之前嗎?是 A 指向 B !!
由於是多線程同時操作,當所有線程都執行完畢以后,就可能會出現這樣的情況:
此時居然出現了環狀的鏈表結構,如果這個時候去取值,就會出錯——InfiniteLoop(死循環)。
-
結論:
使用頭插會改變鏈表的上的順序,但是如果使用尾插,在擴容時會保持鏈表元素原本的順序,就不會出現鏈表成環的問題了。
Java7在多線程操作HashMap時可能引起死循環,原因是擴容轉移后前后鏈表順序倒置,在轉移過程中修改了原來鏈表中節點的引用關系。
Java8在同樣的前提下並不會引起死循環,原因是擴容轉移后前后鏈表順序不變,保持之前節點的引用關系。
說說resize時,擴容是怎么實現的?
(1)擴容:新建一個Entry空數組,長度是原來的兩倍。
(2)ReHash :遍歷原來的Entry數組,把所有的數據重新Hash到新的數組。
-
為什么要重新Hash呢,直接復制過去不是更快捷方便嗎?
因為擴容以后的數組長度變了,
index = HashCode(Key) & (Length - 1)
,擴容后的length和之前不一樣了,之前是16,現在是32,重新Hash算出來的index值肯定也不一樣,而且重新計算后,會使元素更加均勻的分布在HashMap表中,如果直接復制的話,那么數據肯定都堆在一起了。
為什么HashMap初始化時默認容量是16?為什么要是2的n次冪?
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 ,
有幾方面的原因:
-
根據上述官方的定義可知,16 = 2^4,也就是提醒我們這個數最好設置為 2 的N次冪。
-
在計算index的時候:
index = (n - 1) & hash
,n就是數組的長度,這里就是16,而16-1 = 15 ,15的二進制數為:1111,假設hash值為:01011101100011,則:
01011101100011 & 1111 --------------------- index: 0011
可以看出,15的二進制全都是1,假設默認值是8,則8-1=7 ,7的二進制是 111,再假設默認值是32,
32-1 = 31 ,31的二進制 11111。可以發現這樣的規律,所有2的n次冪的數減一的二進制所有位都是1。
這樣一來,在計算index的時候,就只和hashCode的最后幾位有關,這樣可以極大的提高效率。
而至於為什么非要用 16 而不用8或者32,官方也沒有具體說明,所以這個應該只是一個經驗數字,除了他滿足是2的冪以外,不大也不小,剛好合適,能滿足日常大部分需求。
-
index計算的時候,為什么要用位運算
&
呢?主要是效率問題,位運算(&)效率要比代替取模運算(%)高很多,主要原因是位運算直接對內存數據進行操作,不需要轉成十進制,因此處理速度非常快。在jdk1.8之前的index計算就是用的取模運算:
%
.
為什么加載因子是0.75f?
加載因子太大的話,也就是說需要盡可能的把HashMap表填滿了才進行擴容,那這樣會使得hash沖突的 概率增大,但是如果加載因子太小,那只用了很小一部分空間就要開始擴容,使得空間利用率很低。那怎 么辦平衡hash碰撞和空間利用率這個問題,這就是問題的關鍵!!
根據官方源碼的注釋可以看到:
threshold of 0.75, although with a large variance because of
* resizing granularity. Ignoring variance, the expected
* occurrences of list size k are (exp(-0.5) * pow(0.5, k) /
* factorial(k)). The first values are:
*
* 0: 0.60653066
* 1: 0.30326533
* 2: 0.07581633
* 3: 0.01263606
* 4: 0.00157952
* 5: 0.00015795
* 6: 0.00001316
* 7: 0.00000094
* 8: 0.00000006
* more: less than 1 in ten million
大概意思就是說,在理想情況下,使用隨機哈希碼,節點出現的頻率在hash桶中遵循泊松分布,同時給出了桶中元素個數和概率的對照表。從上面的表中可以看到當桶中元素到達8個的時候,概率已經變得非常小,也就是說用0.75作為加載因子,每個碰撞位置的鏈表長度超過8個的概率達到了一百萬分之一。
即加載因子為0.75,同一個桶中出現8個元素然后轉化為紅黑樹的概率為100萬分之一。
HashMap的主要構造器都有哪些?
- HashMap():構建一個初始容量為 16,負載因子為 0.75 的 HashMap
- HashMap(int initialCapacity):構建一個初始容量為 initialCapacity,負載因子為 0.75 的 HashMap
- HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的負載因子創建一個 HashMap。
HashMap的主要屬性參數:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默認數組長度2^4=16
static final int MAXIMUM_CAPACITY = 1 << 30; // 最大數組容量2^30
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默認負載因子
static final int TREEIFY_THRESHOLD = 8; // 鏈表轉紅黑樹的閾值
static final int UNTREEIFY_THRESHOLD = 6; // 擴容時紅黑樹轉鏈表的閾值
最后:
限於筆者水平有限,難免有些不足或錯漏之處,歡迎各位大佬批評指出,不勝感激~~
個人網站:https://www.coding-makes-me-happy.top/