HashMap
HashMap的存儲結構
說到這裏大家都能脫口而出存儲的是鍵值對,可是鍵值對是怎麼存儲的很多人都不太瞭解。
我們需要對HashMap內部三種數據結構進行了解。
- 普通節點(鏈表)
- 紅黑樹節點
- 節點數組(桶)
普通節點
HashMap中維護的這個內部類實現了Entry接口,其實就是我們所熟悉的鍵值對,並且由於該類裏面有指向下一個節點的引用,所以可以把這個節點理解爲一個鏈表
// 這個節點就是我們使用的鍵值對
static class Node<K,V> implements Map.Entry<K,V> {
// 鍵值對key值的hash(這裏面的hash是經過處理過的)
final int hash;
// 鍵值對的key值
final K key;
// 鍵值對的value值
V value;
// 下一個節點
Node<K,V> next;
}
紅黑樹節點
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
// 父節點
TreeNode<K,V> parent; // red-black tree links
// 左孩子
TreeNode<K,V> left;
// 又孩子
TreeNode<K,V> right;
// 刪除時取消連接用的
TreeNode<K,V> prev; // needed to unlink next upon deletion
// 標識是紅節點還是黑節點
boolean red;
}
節點數組
該數組裏面存放的是普通節點的頭結點,或者紅黑樹的根節點。
因爲TreeNode繼承於LinkedHashMap.Entry,而LinkedHashMap.Entry繼承於HashMap.Node
總結
- HashMap內部維護了一個桶,桶內存放的是若干鏈表及紅黑樹。
HashMap的存操作
該操作可以分爲下面幾個步驟:
- 計算key值hash,判斷應該存放在桶內哪個位置
- 將該鍵值對放在對應位置鏈表的末尾
- 判斷該鏈表長度大於等於8時,將該鏈表轉化爲紅黑樹
- 判斷桶內鏈表及紅黑樹的個數和是否大於當前桶的大小*加載因子(默認0.75)
- 大於則rehash擴容桶的大小
下面我們針對這幾個步驟瞭解一下里面的處理邏輯
計算key值hash
下面是計算hash的源碼
static final int hash(Object key) {
int h;
// 從這裏可以說明hashmap的key值可以爲null,爲null的hash默認爲0
// hashtable取hash是用的下面兩句話,這也解釋了爲什麼hashtablekey值不能爲空
// Entry<?,?> tab[] = table;
// int hash = key.hashCode();
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
可以分爲下面幾個步驟
- 計算key的hash值
- 將hash值向右無符號位移16位
- 原hash與右移過的hash位異或運算
第一步到第三步稱爲hashmap中的擾動函數,爲了就是減少hash碰撞
爲什麼要右移16位
結論:因爲右移16位可以讓高位參與運算。
原理:
- hashCode()函數調用的是key鍵值類型自帶的哈希函數,返回int型散列值。
- int類型爲2進制32位的,右移16位看好是的前16位高半區設0,高半區設到後16位低半區
爲什麼要原hash與右移過的hash位異或運算
結論:使得低半區同時具有高半區和低半區的特性。
原理:
- 位移過後低半區的值不在保留,所有隻有高半區的特性。
- 只有經過異或運算纔可以加大低位的隨機性。而且混合後的低位摻雜了高位的部分特徵,這樣高位的信息也被變相保留下來。
判斷應該存放在桶內哪個位置
HashMap是通過將經過擾動算法計算出的hash值與當前桶的大小減一再進行與運算得出來的一個數組下標
爲什麼要與運算
其實這個沒啥原理啥的。。。就因爲內存不夠大,上面也提到過hashcode是個表值範圍是從-2147483648到2147483648。前後加起來大概40億的映射空間如果你內存足夠大能發下40億長度的數組,鬼才做擾動。所以只能儘可能的減少碰撞,出現碰撞再轉化爲鏈表或者紅黑樹。
順便說一下,這也正好解釋了爲什麼HashMap的數組長度要取2的整次冪。因爲這樣(數組長度-1)正好相當於一個“低位掩碼”。“與”操作的結果就是散列值的高位全部歸零,只保留低位值,用來做數組下標訪問。以初始長度16爲例,16-1=15。2進製表示是00000000 00000000 00001111。和某散列值做“與”操作如下,結果就是截取了最低的四位值。
將該鍵值對放在對應位置鏈表的末尾
在1.8之前都是從頭部開始插入的,但是從頭部開始插入,會使得擴容時該錶鏈表的位置在併發情況下會形成環,所以從1.8之後改成了從尾部開始。
判斷該鏈表長度大於等於8時,將該鏈表轉化爲紅黑樹
因爲鏈表如果太長的話會會降低查詢效率,因爲鏈表平均查找長度爲n/2。
紅黑樹平均查找長度爲logn。
所以如果鏈表長度過長的話爲了增加查詢效率則需要轉化爲紅黑樹。而選擇8這個閾值是因爲
log8 = 3。 8/2 =4。超過8紅黑樹的查詢效率就要高於數組。
並且紅黑樹還需要左旋右旋,所以節點的少的時候鏈表更優。
判斷是否需要擴容
當hashmap中的元素越來越多的時候,碰撞的機率也就越來越高(因爲數組的長度是固定的),所以爲了提高查詢的效率,就要對hashmap的數組進行擴容,數組擴容這個操作也會出現在ArrayList中,所以這是一個通用的操作,很多人對它的性能表示過懷疑,不過想想我們的“均攤”原理,就釋然了,而在hashmap數組擴容之後,最消耗性能的點就出現了:原數組中的數據必須重新計算其在新數組中的位置,並放進去,這就是resize。
那麼hashmap什麼時候進行擴容呢?當hashmap中的元素個數超過數組大小loadFactor時,就會進行數組擴容,loadFactor的默認值爲0.75,也就是說,默認情況下,數組大小爲16,那麼當hashmap中元素個數超過160.75=12的時候,就把數組的大小擴展爲216=32,即擴大一倍,然後重新計算每個元素在數組中的位置,而這是一個非常消耗性能的操作,所以如果我們已經預知hashmap中元素的個數,那麼預設元素的個數能夠有效的提高hashmap的性能。比如說,我們有1000個元素new HashMap(1000), 但是理論上來講new HashMap(1024)更合適,不過上面annegu已經說過,即使是1000,hashmap也自動會將其設置爲1024。 但是new HashMap(1024)還不是更合適的,因爲0.751000 < 1000, 也就是說爲了讓0.75 * size>1000, 我們必須這樣new HashMap(2048)才最合適,既考慮了&的問題,也避免了resize的問題。