1 什麼是hash衝突 我們知道HashMap底層是由數組+鏈表/紅黑樹構成的,當我們通過put(key, value)向hashmap中添加元素時,需要通過散列函數確定元素究竟應該放置在數組中的哪個位置,當不同的元素被放置在了數據的同一個位置時,後放入的元素會以鏈表的形式,插在前一個元素的尾部,這 ...
1 什麼是hash衝突
我們知道HashMap底層是由數組+鏈表/紅黑樹構成的,當我們通過put(key, value)
向hashmap中添加元素時,需要通過散列函數確定元素究竟應該放置在數組中的哪個位置,當不同的元素被放置在了數據的同一個位置時,後放入的元素會以鏈表的形式,插在前一個元素的尾部,這個時候我們稱發生了hash衝突。
2 如何解決hash衝突
事實上,想讓hash衝突完全不發生,是不太可能的,我們能做的只是儘可能的降低hash衝突發生的概率:下麵介紹在HashMap中是如何應對hash衝突的?
當我們向hashmap中put元素(key, value)時,最終會執行putVal()
方法,而在putVal()
方法中,又執行了hash(key)
這個操作,並將執行結果作為參數傳遞給了putVal方法。那麼我們先來看hash(key)
方法幹了什麼。
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
// 判斷key是否為null, 如果為null,則直接返回0;
// 如果不為null,則返回(h = key.hashCode()) ^ (h >>> 16)的執行結果
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
(h = key.hashCode()) ^ (h >>> 16)
執行了三步操作 :我們一步一步來分析:
第1步:h = key.hashCode()
這一步會根據key值計算出一個int類型的h值也就是hashcode值,例如
"helloWorld".hashCode() --> -1554135584
"123456".hashCode() --> 1450575459
"我愛java".hashCode() --> -1588929438
至於hashCode()
是如何根據key計算出hashcode值的,要分幾種情況進行分析:
- 如果我們使用的自己創建的對象,在我們沒有重寫hashCode()方法的情況下,會調用Object類的hashCode()方法,而此時返回就是對象的記憶體地址值,所以如果對象不同,那麼通過hashcode()計算出的hashcode就是不同的。
- 如果是使用java中定義的引用類型例如String,Integer等作為key,這些類一般都會重寫hashCode()方法,有興趣可以翻看一下對應的源碼。簡單來說,Integer類的hashCode()返回的就是Integer值,而String類型的hashCode()方法稍稍複雜一點,這裡不做展開。總的來說,hashCode()方法的作用就是要根據不同的key得到不同的hashCode值。
JDK 8 系列教程:
第2步:h >>> 16
這一步將第1步計算出的h值無符號右移16位。
為什麼要右移16位,當然是位了第三步的操作。
第3步:h ^ (h >>> 16)
將hashcode值的高低16位進行異或操作(同0得0、同1得0、不同得1)得到hash值,舉例說明:
- 假設h值為:1290846991
- 它的二進位數為:01001100 11110000 11000011 00001111
- 右移十六位之後:00000000 00000000 01001100 11110000
- 進行異或操作後:01001100 11110000 10001100 11110000
- 最終得到的hash值:1290833136
那麼問題來了: 明明通過第一步得到的hashcode值就可以作為hash返回,為什麼還要要進行第二步和第三步的操作呢?答案是為了減少hash衝突!
元素在數組中存放的位置是由下麵這行代碼決定的:
// 將(數組的長度-1)和hash值進行按位與操作:
i = (n - 1) & hash // i為數組對應位置的索引 n為當前數組的大小
我們將上面這步操作作為第4步操作,來對比一下執行1、2、3、4四個步驟和只執行第1、4兩個步驟所產生的不同效果。
我們向hashmap中put兩個元素node1(key1, value1)
、node2(key2, value2)
,hashmap的數組長度n=16
。
執行1、2、3、4 四個步驟:
1.h = key.hashCode()
- 假設計算的結果為:
h = 3654061296
- 對應的二進位數為:
01101100 11100110 10001100 11110000
2.h >>> 16
- h無符號右移16位得到:
00000000 00000000 01101100 11100110
3.hash = h ^ (h >>> 16)
- 異或操作後得到hash:
01101100 11110000 11100000 00000110
4.i = (n-1) & hash
- n-1=15 對應二進位數 :
00000000 00000000 00000000 00001111
- hash :
01101100 11110000 11100000 00000110
- hash & 15 :
00000000 00000000 00000000 00000110
- 轉化為10進位 :
&ensp 5
最終得到i的值為5,也就是說node1存放在數組索引為5的位置。
同理我們對(key2, value2) 進行上述同樣的操作過程:
1.h = key.hashCode()
- 假設計算的結果為:
h = 3652881648
- 對應的二進位數為:
01101100 11011101 10001100 11110000
2.h >>> 16
- h無符號右移16位得到:
00000000 00000000 01101100 11011101
3.hash = h ^ (h >>> 16)
- 異或操作後得到hash:
01101100 11110000 11100000 00101101
4.i = (n-1) & hash
- n-1=15 對應二進位數 :
00000000 00000000 00000000 00001111
- hash :
01101100 11110000 11100000 00101101
- hash & 15 :
00000000 00000000 00000000 00001101
- 轉化為10進位 :
&ensp 13
最終得到i的值為13,也就是說node2存放在數組索引為13的位置
node1和node2存儲的位置如下圖所示:
執行1、4兩個步驟:
1.h = key.hashCode()
- 計算的結果同樣為:
h = 3654061296
- 對應的二進位數為:
01101100 11100110 10001100 11110000
4.i = (n-1) & hash
- n-1=15 對應二進位數 :
00000000 00000000 00000000 00001111
- hash(h) :
01101100 11100110 10001100 11110000
- hash & 15 :
00000000 00000000 00000000 00000000
- 轉化為10進位 :
0
最終得到i的值為0,也就是說node1存放在數組索引為0的位置
同理我們對(key2, value2) 進行上述同樣的操作過程:
1.h = key.hashCode()
- 計算的結果同樣為:
h = 3652881648
- 對應的二進位數為:
01101100 11011101 10001100 11110000
4.i = (n-1) & hash
- n-1=15 對應二進位數 :
00000000 00000000 00000000 00001111
- hash(h) :
01101100 11110000 11100000 11110000
- hash & 15 :
00000000 00000000 00000000 00000000
- 轉化為10進位 :
0
最終得到i的值為0,也就是說node2同樣存放在數組索引為0的位置
node1和node2存儲的位置如下圖所示:
相信大家已經看出區別了:
當數組長度n較小時,n-1的二進位數高16位全部位0,這個時候如果直接和h值進行&
(按位與)操作,那麼只能利用到h值的低16位數據,這個時候會大大增加hash衝突發生的可能性,因為不同的h值轉化為2進位後低16位是有可能相同的,如上面所舉例子中:key1.hashCode()
和key2.hashCode()
得到的h值不同,一個h1 = 3654061296
,另一個h2 = 3652881648
,但是不幸的是這h1、h2兩個數轉化為2進位後低16位是完全相同的,所以h1 & (n-1)
和 h2 & (n-1)
會計算出相同的結果,這也導致了node1和node2 存儲在了數組索引相同的位置,發生了hash衝突。
當我們使用進行 h ^ (h >>> 16)
操作時,會將h的高16位數據和低16位數據進行異或操作,最終得出的hash值的高16位保留了h值的高16位數據,而hash值的低16數據則是h值的高低16位數據共同作用的結果。所以即使h1和h2的低16位相同,最終計算出的hash值低16位也大概率是不同的,降低了hash衝突發生的概率。
ps:這裡面還有一個值的註意的點: 為什麼是(n-1)?
我們知道n是hashmap中數組的長度,那麼為要進行n-1的操作?答案同樣是為了降低hash衝突發生的概率!
要理解這一點,我們首先要知道HashMap規定了數組的長度n必須為2的整數次冪,至於為什麼是2的整數次冪,會在HashMap的擴容方法resize()
里詳細講。
既然n為2的整數次冪,那麼n一定是一個偶數。那麼我們來比較i = hash & n
和 i = hash & (n-1)
有什麼異同。
n為偶數,那麼n轉化為2進位後最低位一定為0,與hash進行按位與操作後最低位仍一定為0,這就導致i值只能為偶數,這樣就浪費了數組中索引為奇數的空間,同時也增加了hash衝突發生的概率。
所以我們要執行n-1,得到一個奇數,這樣n-1轉化為二進位後低位一定為1,與hash進行按位與操作後最低位即可能位0也可能位1,這就是使得i值即可能為偶數,也可能為奇數,充分利用了數組的空間,降低hash衝突發生的概率。
至此, JDK1.8中 HashMap 是如何在存儲元素時減少hash發生就講解完畢了!
來源:blog.csdn.net/weixin_43689776/article/details/99999126
近期熱文推薦:
1.1,000+ 道 Java面試題及答案整理(2022最新版)
4.別再寫滿屏的爆爆爆炸類了,試試裝飾器模式,這才是優雅的方式!!
覺得不錯,別忘了隨手點贊+轉發哦!