HashMap1.8hash碰撞 和 擴容方法

爲何要學習hashMap的源碼
因爲集合在我們工作和學習過程中都非常常見,且代碼寫的非常優雅,如果想要得到一份高工資的工作,且現在市
面上jdk1.8已經流行起來了,相信面試過程中越來越多的面試官會詢問源碼的知識,所有源碼是我們必須去弄懂
的,接下來我們就來一起學習hashMap1.8的源源碼前的設想問題
思考的問題
1. hashMap初始化大小是多少
2. hashMap結構是什麼樣子
3. 如果已經得知hashMap的數據結構是鏈表加數組,那我們如何去避免hash碰撞
4. hashMap在什麼時候擴容
5. 擴容的的方式是什麼
正式篇
hashMap的結構
1.在1.8中,hashMap的結構分成鏈表加數組和數組+紅黑樹,因爲1.8的作者考慮到鏈表沒有索引,遍歷效率低
下,所以當鏈表長度大於 8 - 1 也就是 7 時,會轉化成紅黑樹,那麼當長度不足6時,又會將紅黑樹轉化成鏈表 在
源碼中 描述數據結構的樣子是transient Node<K,V>[] table; 而node是一個單鏈表對象,所以結構是數組+鏈表  
 

[Java] 純文本查看 複製代碼
1
2
static final int TREEIFY_THRESHOLD = 8; 樹的臨界點
static final int UNTREEIFY_THRESHOLD = 6;非樹的臨界點


hash碰撞問題
1.既然已經知道了hashMap的結構,那麼接下來,我們就來看看他如何放置hash碰撞問題,按照我們的設想,我
們可以讓其對數組的長度取模,比如,數組的長度是16,我們可以用key的hash值對數組取模,取到的值是0-15正
好能落在數組的位置上,但這種方式並不能保證數字能夠儘量分散的落在數組上,而過多的元素落在同一個節點上
就會導致形成的鏈表長度過長,而影響hashmap的取值速度,所以我們現在就來看看源碼中是如何實現的

[Java] 純文本查看 複製代碼
01
02
03
04
05
06
07
08
09
10
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
} i
f ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16


綜合來看,此方法兩個參數參與了運算
option1:hash值
option2:數組的長度-1 那麼數組的長度是 ,且在源碼註釋處已經指出,數組的長度需要是2^n次冪,即偶數
一個int型的hash值 ,我們可以隨機假設一個:
010101010101001010101010101010
那麼它與一個偶數或者一個非偶數取&操作會是什麼結果呢?
如果是和奇數取&那麼得到的結果即可能是奇數,也可能是偶數,而如果和一個偶數&那麼答案只能是一個偶數,
所以答案很明顯,只有當數組的長度是偶數即2的n次冪時,此時才能保證得到的數字即可能是偶數也可能是奇數
那我們再來看第一個值,第一個值是hash值,hash值只能和數組的長度-1 運算,那麼如果是假設直接拿hash值和
數組計算,相當於這個hash值只有幾位參與了運算,其他位並沒有參與運算,這樣做可能也會使得不同的元素,得
到相同的結果(即最後幾位相等,但前幾位不同),在源碼中採用的辦法是將hash值向右移動16位,得到他的高16
位,同時和低16 位取異或,這樣就能保證整個hash值都參與了運算,那爲啥取異或呢?因爲異或可以使得得到的
0,1 二進制儘量的平均
舉例
0 0 1 1
0 1 0 1 取&
0 0 0 1 0 的概率0.75 1 的概率0.25
0 0 11
0 1 0 1 取|
0 1 1 1 0 的概率0.25 1 的概率0.75
0 0 1 1
0 1 0 1 取^
0 1 1 0 的概率 0.5 1的概率0.5
hash的擴容方法
在 1.8中hashMap的擴容方法是由 resize方法決定的
此方法兩個作用
1.初始化
2.擴容

[Java] 純文本查看 複製代碼
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
{
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 { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
} e
lse {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
} i
f (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}


此方法就是決定擴容核心代碼
首先遍歷這個數組,遍歷過程中有三段邏輯
1.看這個數組上的元素是否爲null,如果是null,那麼採用尾插法(1.7採用的是頭插法,但頭插在多線程情況下可能
出現鏈表的死循環,這裏不作多解釋)
2.如果是紅黑樹,那麼按照紅黑樹的處理方式處理
3.如果數組上有元素,按照我們的想法,我們應當是重新計算這個key在數組中的位置
最核心的代碼: 是 e.hash & oldCap
我們發現當它在計算時,它並沒有直接和oldCap -1 計算& 而是直接和數組的長度計算
那麼我們再次帶入這個計算的方式看看這個精妙的算法是怎麼回事:
當第一次擴容時,執行的resize方法

[Java] 純文本查看 複製代碼
1
2
3
4
5
6
7
8
9
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
} e
lse if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}


此時數組的長度已經擴大了兩倍
新長度是32 ,老長度是16
0101010101010010101010101 0 1 0 1 0
0 1 1 1 1 老長度-1 //1
1 0 0 0 0 老長度 //2
0 1 1 1 1 1 新長度-1 //3
用 1,3對比,我們可以發現,數組移動不移動是看第五位是否是0 ,如果是0 ,不移動,如果不是0 ,新長度的值要
比原來的值 大16
用2,3 對比,如果第五5位是0,那麼得到的結果就是0 ,如果第五位非0 ,那麼得到的結果就不是0
1,3移動---> 第五位非0 ,且移動16 位 ---> 第五位 0 , 不移動
2,3 ---> 第五位非0 ,知道要移動了,且在源碼中可以發現,移動了16
第五位 0 ,不移動

[Java] 純文本查看 複製代碼
1
2
3
4
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}


所以移動不移動,只需要看倒數第五位即可,不得不說,hashMap這個設計很精妙
結束語
相信通過剛纔的學習,同學們已經對hash的碰撞問題和hash的擴容方法有了一個具體的認識,希望大家繼續認真
學習。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章