HashMap
的源碼很多也很複雜,本文只是摘取簡單常用的部分代碼進行分析。能力有限,歡迎指正。
HASH 值的計算
前置知識——位運算
按位異或操作符^
:1^1=0, 0^0=0, 1^0=0, 值相同爲0,值不同爲1。按位異或就是對二進制中的每一位進行異或運算。
1111 0000 1111 1110
^ 1111 1111 0000 1111
______________________
0000 1111 1111 0001
按位右移補零操作符>>>
:左操作數按右操作數指定的位數右移,移動得到的空位以零填充。
1110 1101 1001 1111
>>> 4
___________________________
0000 1110 1101 1001
擾動函數
爲什麼要做擾動?
理論上哈希值是一個int
類型,如果直接拿哈希值做下標的話,考慮到2進制32位帶符號的int表值範圍從-2147483648到2147483648。前後加起來大概40億的映射空間。這麼大的數組,內存是存不下的,所以這個散列值是不能直接拿來用的。用之前還要先做對數組的長度取模運算,得到的餘數才能用來訪問數組下標。
因爲只取最後幾位,所以哈希碰撞的可能性大大增加,這時候擾動函數的價值就來了。
擾動計算
先調用hashCode()
方法得出hash值,再進行擾動操作。
右位移16位,正好是32bit的一半(int
是32位的),自己的高半區和低半區做異或,就是爲了混合原始哈希碼的高位和低位,以此來加大低位的隨機性。而且混合後的低位摻雜了高位的部分特徵,這樣高位的信息也變相保留下來。
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
取模,計算出下標
在計算下標的時候,讓列表長度對哈希值做取模操作,讓計算出來的哈希值在列表範圍內,n 爲list
長度
i = (n - 1) & hash
爲什麼HashMap
的數組長度要取2的整次冪
因爲這樣(數組長度 - 1)正好相當於一個“低位掩碼”。&
操作的結果就是散列值的高位全部歸零,只保留低位值,用來做數組的下標訪問。以初始長度16爲例,16-1=15,2進製表示是0000 1111。和某散列值做&
操作如下:
1010 0011 0110 1111 0101
& 0000 0000 0000 0000 1111
____________________________
0000 0000 0000 0000 0101
是什麼存入了 table
HashMap
存入table
的值並不只有value,而是構造成一個 Node 對象實例存入 table。
Node對象裏有:hash, key, value, next(哈希衝突時的鏈表)
理論最大容量
int MAXIMUM_CAPACITY = 1 << 30;
2的30次方
負載因子
負載因子是用來計算負載容量(所能容納的最大Node個數)的,當前list長度 length,負載因子 loadFactor
負載容量計算公式爲:
threshold = length * loadFactor
默認負載因子爲 0.75。也就是說,當Node個數達到當前list長度的75%時,就要進行擴容,否則會增加哈希碰撞的可能性。負載因子的作用是在空間和時間效率上取得一個平衡。
float DEFAULT_LOAD_FACTOR = 0.75f
擴容做了哪些操作
- 創建一個新的Entry空數組,長度是原數組的2倍。
當Node個數超過負載容量時,進行擴容。
old << 1
左移一位相當於 old * 2。
重新Hash
遍歷原Entry數組,把所有的Entry重新Hash到新數組中。
爲什麼要重新hash?因爲長度擴大以後,hash值也隨之改變(數組下標的計算是數組長度對hashcode進行取模)。
這樣就可以把原先哈希衝突的鏈表拉平,使數組變得稀疏。
final Node<K,V>[] resize() {
// 保存現有的數組
Node<K,V>[] oldTab = table;
// 保存現有的數組長度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 保存現有的負載容量
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
// 如果現有容量已經超過最大值了,就沒辦擴容了,只好隨你碰撞了
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 原有容量左移一位,相當於 oldCap * 2
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
// 負載容量也擴大一倍
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
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) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 遍歷數組中所有元素,重新進行hash
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
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 { // 優化鏈表
// 把原有鏈表拆成兩個鏈表
// 鏈表1存放在低位(原索引位置)
Node<K,V> loHead = null, loTail = null;
// 鏈表2存放在高位(原索引 + 舊數組長度)
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
// 鏈表1
// 這個位運算的原理可以參考第三篇參考資料
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
// 鏈表2
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 鏈表1存放於原索引位置
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 鏈表2存放原索引加上舊數組長度的偏移量
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
樹化改造
鏈表長度太長,會被改造成紅黑樹。
當鏈表的長度超過MIN_TREEIFY_CAPACITY
最大樹化臨界值,就會進行樹化改造。
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
...
}
}
爲什麼要樹化?
本質上是個安全問題。因爲鏈表查詢影響性能,如果有人惡意造成哈希碰撞,就會構成哈希碰撞拒絕服務攻擊,服務端CPU被大量佔用用於鏈表查詢,造成服務變慢或不可用。
源碼系列文章
參考
JDK 源碼中 HashMap 的 hash 方法原理是什麼?胖君的回答
本文首發於我的個人博客 http://chaohang.top
作者張小超
轉載請註明出處
歡迎關注我的微信公衆號 【超超不會飛】,獲取第一時間的更新。