HashMap實現原理
-
HashMap
是在JDK1.2中引入的一種K/V對
形式的集合類. - 在底層,
HashMap
通過 陣列和單鏈表 組合的結構形式來儲存資料,陣列在這作為一個外部結構,陣列中的每個節點被稱做Bucket(桶)
,而 桶是由在單鏈表構成 ,JDK1.8
之後 為了解決長連結串列下,查詢和插入效率低下的情況,又引入了紅黑樹的作為桶的實現方式 , - 桶中的各節點是由
HashMap
定義的Node
內部類生成的,是普通的連結串列節點類.

- 注意:
HashMap
是執行緒不安全的,多執行緒情況下可能會出現環路(後面會講) ,多執行緒狀態下還是使用ConcurrentHashMap
比較合適.
重點引數
-
HashMap
的引數不多,除去當做預設屬性的靜態常量和底層陣列物件,就只有以下五個
transient Node<K,V>[] table; transient int size transient int modCount; int threshold; final float loadFactor; 複製程式碼
-
table
就是整個HashMap
的底層陣列,table
的初始化並不在建構函式中完成,而是在resize()
方法中完成.-
table
的初始化可能有點繞,建構函式中最多指定了閾值threshold
和負載因子loadFactor
並沒有容量相關,但是在resize()
方法中 會根據舊容量和舊閾值判斷新容量是等於預設容量,舊閾值或者兩倍舊容量 ,最後根據新容量建立新陣列
-
-
loadFactor
就是所謂的負載因子,預設為0.75,是控制擴容時機的關鍵屬性,因為擴容發生在當前元素個數超過閾值時,而閾值等於當前容量乘以負載因子. -
modCount
為修改計數,是fast-fail
機制的關鍵引數.在對Map
中的元素做新增/刪除操作時會自增,但修改不會(putVal()方法中覆蓋原值)
新增邏輯
-
HashMap
的新增過程重點主要還是定位, 如何確定元素在陣列中的位置 ,HashMap
採用的就是 Hash演算法- 首先
HashMap
會根據Key
的hash值,按照表達式(n - 1) & hash
計算出桶的下標 - 如果此時桶為空,會建立一個新的
Node
,作為連結串列的第一個元素,直接存放在陣列中.(以前還聽說過什麼連結串列首節點為空的情況,是假的.) - 如果節點存在又會區分樹節點(TreeNode)和普通節點(Node)兩種情況.
TreeNode
- 首先
- 另外 新增前會判斷底層陣列
table
是否初始化,新增後會判斷該桶大小是否超過的8,超過則轉化為紅黑樹,再判斷整個陣列是否需要擴容. -
Hash
同時也叫雜湊,可以把任意長度的輸入通過演算法,換算成固定長度的輸出,不同元素通過Hash
演算法獲得的下標一致可以被稱之為衝突或者碰撞
,Hash
演算法的要求就是使元素儘量少的發生碰撞,從而均勻的散佈在陣列中
.而發生碰撞時,像HashMap
這種以一個列表下掛的方式可以被稱為拉鍊法
.
查詢邏輯
- 此處的查詢邏輯是指呼叫
get()
方法,通過key
值查詢的情況,如果自己遍歷的另說.- 同樣是根據表示式
(n - 1) & hash
計算出桶的下標(可以說是相當重要了),若得到的桶為空,直接返回null - 不為空時則會遍歷整個桶,並根據
key.equals(k)
判斷是否相等 - 遍歷的方法也會根據節點型別的不同而不同,但是區分節點前直接存放在陣列中的頭結點是要先進行判斷的.感覺上效能影響不大吧
- 同樣是根據表示式
- 從查詢的過程可以看出,確定桶下標的計算不存在隨機性,時間複雜度就為O(1),具體的效能體現在遍歷這一塊,連結串列查詢的時間複雜度為O(n),所以連結串列越長遍歷時間也就越長,插入和查詢的效率也就越低.所以在
JDK1.8
之後引入的紅黑樹作為桶的另一種實現方法.當連結串列長度大於8
時,桶的實現會轉化為紅黑樹
. -
HashMap
的效能很大一部分取決於Hash
演算法.
RESIZE邏輯
-
通過插入和查詢我們可以知道,在陣列大小不變的情況下,**連結串列越長或者說樹的高度越高效能都會降低,**所以此時很有必要通過擴容陣列的方式,重新排列桶中元素,降低連結串列長度,減少樹的高度.
-
首先,觸發擴容的情況是
size > threshold
即元素個數大於閾值.整個擴容過程可以簡單的拆分為以下幾步:- 對陣列進行擴充,一般情況下是 陣列容量和閾值都變為原來的兩倍 .
- 此間會有上限判斷,容量最大為
1 << 30
也就是1的30次方.
- 此間會有上限判斷,容量最大為
- 遍歷舊陣列,重新判斷元素的位置並散佈到新陣列.
- 對陣列進行擴充,一般情況下是 陣列容量和閾值都變為原來的兩倍 .
-
resize()
方法中重新散佈元素的方法還是很有意思的除去單元素連結串列和紅黑樹(桶的容量在1~7之間)- 首先將新陣列分為兩部分
lo
和hi
(原始碼是loHead和hiHead,我猜時low和high,怎麼這麼縮寫隨意),lo
表示0到舊容量部分,hi
表示餘下算是新加入的部分,並以此建立兩個連結串列 - 根據表示式
e.hash & oldCap
判斷元素是否分佈在lo
部分,是就掛到lo
連結串列下面,否就掛到hi
連結串列下面. -
lo
連結串列掛到和舊陣列相同位置的桶,而hi
則掛到下標為原下標 + 舊陣列容量
的桶.- 此處的依據就是
e.hash & (oldCap - 1) + oldCap == e.hash & (oldCap << 1) -1
- 此處的依據就是
- 首先將新陣列分為兩部分
-
可以看出
resize()
方法會調整全部的元素雜湊情況,因此過於頻繁的resize
會降低HashMap
的效能, 因此如果一開始可以大概知道所需要存放的元素個數時,儘量直接指定容量大小. -
JDK1.7
之前的resize()
方法在併發條件下可能會發生閉環問題,但在JDK1.8
之後不會在出現,但並不代表HashMap
可以在併發條件下使用了,小部分情況還是會出現資料丟失等問題. -
介紹
JDK1.8
之前的閉環問題詳情的文章- ofollow,noindex">疫苗:JAVA HASHMAP的死迴圈