上一篇中對HashMap中的基本內容做了詳細的介紹,解析了其中的get和put方法,想必大家對於HashMap也有了更好的認識,本篇將從了算法的角度,來分析HashMap中的那些函數。
HashCode
先來說說HashMap中HashCode的算法,在上一篇里,我們看到了HashMap中的put方法是這樣的:
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
那這個hash函數又是什么呢?讓我們來看看它的真面目:
/** * 將高位與低位進行與運算來計算哈希值。因為在hashmap中使用2的整數冪來作為掩碼,所以只在當前掩碼之上的位上發生 * 變化的散列總是會發生沖突。(在已知的例子中,Float鍵的集合在小表中保持連續的整數)因此,我們應用一個位運算 * 來向下轉移高位的影響。 這是在綜合考慮了運算速度,效用和質量之后的權衡。因為許多常見的散列集合已經合理分布 * (所以不能從擴散中受益),並且因為我們使用樹來處理bin中發生的大量碰撞的情況,所以我們盡可能以代價最低的方式 * 對一些位移進行異或運算以減少系統損失, 以及合並由於hashmap容量邊界而不會被用於散列運算的最高位的影響。 * * todo 擾動函數 */ static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
可以看出,這里並不是簡單的使用了key的hashCode,而是將它的高16位與低16位做了一個異或操作。(“>>>”是無符號右移的意思,即右移的時候左邊空出的部分用0填充)這是一個擾動函數,具體效果后面會說明。接下來再看看之前的putval方法:
1 final V putVal(int hash, K key, V value, boolean onlyIfAbsent, 2 boolean evict) { 3 Node<K,V>[] tab; Node<K,V> p; int n, i; 4 //如果當前table未初始化,則先重新調整大小至初始容量 5 if ((tab = table) == null || (n = tab.length) == 0) 6 n = (tab = resize()).length; 7 //(n-1)& hash 這個地方即根據hash求序號,想了解更多散列相關內容可以查看下一篇 8 if ((p = tab[i = (n - 1) & hash]) == null) 9 //不存在,則新建節點 10 tab[i] = newNode(hash, key, value, null); 11 else { 12 Node<K,V> e; K k; 13 //先找到對應的node 14 if (p.hash == hash && 15 ((k = p.key) == key || (key != null && key.equals(k)))) 16 e = p; 17 else if (p instanceof TreeNode) 18 //如果是樹節點,則調用相應的putVal方法,這部分放在第三篇內容里 19 //todo putTreeVal 20 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 21 else { 22 //如果是鏈表則之間遍歷查找 23 for (int binCount = 0; ; ++binCount) { 24 if ((e = p.next) == null) { 25 //如果沒有找到則在該鏈表新建一個節點掛在最后 26 p.next = newNode(hash, key, value, null); 27 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 28 //如果鏈表長度達到樹化的最大長度,則進行樹化,該函數內容也放在第三篇 29 //todo treeifyBin 30 treeifyBin(tab, hash); 31 break; 32 } 33 if (e.hash == hash && 34 ((k = e.key) == key || (key != null && key.equals(k)))) 35 break; 36 p = e; 37 } 38 } 39 //如果已存在該key的映射,則將值進行替換 40 if (e != null) { // existing mapping for key 41 V oldValue = e.value; 42 if (!onlyIfAbsent || oldValue == null) 43 e.value = value; 44 afterNodeAccess(e); 45 return oldValue; 46 } 47 } 48 //修改次數加一 49 ++modCount; 50 if (++size > threshold) 51 resize(); 52 afterNodeInsertion(evict); 53 return null; 54 }
注意看第八行的代碼:
tab[i = (n - 1) & hash]
(n - 1) & hash 即通過key的hash值來取對應的數組下標,並非是對table的size進行取余操作。
那么,為什么要這樣做呢?首先,擾動函數的目的就是為了擴大高位的影響,使得計算出來的數值包含了高 16 位和第 16 位的特性,讓 hash 值更加深不可測
來降低碰撞的概率。從hash方法的注釋中,我們也可以找到答案,一般的散列,其實都是做取余處理,但是HashMap中的table大小是2的整數次冪,也就是說,肯定不是質數,那么在取余的時候,偶數的映射范圍勢必就要小了一半,這樣效果顯然就差很多,而且,除法和取余其實是很慢的操作,所以在JDK8中,使用了一種很巧妙的方式來進行散列。首先,table的大小size設置成了2的整數次冪,這樣使用size-1就變成了掩碼,下面是我找的一張圖,能很好的解釋這個過程:

n是table的大小,默認是16,二進制即為10000,n - 1 對應的二進制則為1111,這樣再與hash值做“與”操作時,就變成了掩碼,除了最后四位全部被置為0,而最后四位的范圍肯定會落在(0~n-1)之間,正好是數組的大小范圍,散列函數的妙處就在於此了。
簡直不能更穩,一波操作猛如虎。
那么我們繼續上一篇的栗子,我們來一步一步分析一下,小明和小李的hash值的映射過程:

小明的hash值是756692,轉換為二進制為10111000101111010100,table的大小是32,n-1=31,對應的二進制為:11111,做“與”運算之后,得到的結果是10100,即為20。
小李的hash值是757012,轉換為二進制為10111000110100010100,與11111做與運算后,得到的結果也是10100,即20,於是就與小明發生了沖突,但還是要先來后到,於是小李就掛在了小明后面。
散列函數看完了,我們接下來再看看擴容函數。
擴容函數
擴容函數其實之前也已經見過了,就在上面的putVal方法里,往上面翻一翻,第六行可以看到resize函數,這就是擴容函數,讓我們來看看它的廬山真面目:
1 /** 2 * 初始化或將table的大小進行擴容。 如果table為null,則按照字段threshold中的初始容量目標進行分配。 3 * 否則,因為我們使用2次冪進行擴容,所以在新表中,來自每個bin中的元素必須保持在相同的索引處,或者以原偏移量的2次冪進行移動。 4 */ 5 final Node<K,V>[] resize() { 6 Node<K,V>[] oldTab = table; 7 int oldCap = (oldTab == null) ? 0 : oldTab.length; 8 int oldThr = threshold; 9 int newCap, newThr = 0; 10 if (oldCap > 0) { 11 if (oldCap >= MAXIMUM_CAPACITY) { 12 threshold = Integer.MAX_VALUE; 13 return oldTab; 14 } 15 //新的容量擴展成原來的兩倍 16 else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && 17 oldCap >= DEFAULT_INITIAL_CAPACITY) 18 //閾值也調整為原來的兩倍 19 newThr = oldThr << 1; // double threshold 20 } 21 else if (oldThr > 0) // initial capacity was placed in threshold 22 newCap = oldThr; 23 else { // zero initial threshold signifies using defaults 24 newCap = DEFAULT_INITIAL_CAPACITY; 25 newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); 26 } 27 if (newThr == 0) { 28 float ft = (float)newCap * loadFactor; 29 newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? 30 (int)ft : Integer.MAX_VALUE); 31 } 32 threshold = newThr; 33 @SuppressWarnings({"rawtypes","unchecked"}) 34 Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; 35 table = newTab; 36 //將舊數組中的node重新散列到新數組中 37 if (oldTab != null) { 38 for (int j = 0; j < oldCap; ++j) { 39 Node<K,V> e; 40 if ((e = oldTab[j]) != null) { 41 oldTab[j] = null; 42 if (e.next == null) 43 newTab[e.hash & (newCap - 1)] = e; 44 else if (e instanceof TreeNode) 45 ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); 46 else { // preserve order 47 Node<K,V> loHead = null, loTail = null; 48 Node<K,V> hiHead = null, hiTail = null; 49 Node<K,V> next; 50 do { 51 next = e.next; 52 if ((e.hash & oldCap) == 0) { 53 if (loTail == null) 54 loHead = e; 55 else 56 loTail.next = e; 57 loTail = e; 58 } 59 else { 60 if (hiTail == null) 61 hiHead = e; 62 else 63 hiTail.next = e; 64 hiTail = e; 65 } 66 } while ((e = next) != null); 67 if (loTail != null) { 68 loTail.next = null; 69 newTab[j] = loHead; 70 } 71 if (hiTail != null) { 72 hiTail.next = null; 73 newTab[j + oldCap] = hiHead; 74 } 75 } 76 } 77 } 78 } 79 return newTab; 80 }
這里可以看到,如果原來的table還未被初始化的話,調用該函數后就會被擴容到默認大小(16),上一篇中也已經說過,HashMap也是使用了懶加載的方式,在構造函數中並沒有初始化table,而是在延遲到了第一次插入元素之后。
當使用put插入元素的時候,如果發現目前的bins占用程度已經超過了Load Factor所設置的比例,那么就會發生resize,簡單來說就是把原來的容量和閾值都調整為原來的2倍,之后重新計算index,把節點再放到新的bin中。因為index值的計算與table數組的大小有關,所以擴容后,元素的位置有可能會調整:

以上圖為例,如果對應的hash值第五位是0,那么做與操作后,得到的序號不會變,那么它的位置就不會改變,相反,如果是1,那么它的新序號就會變成原來的序號+16,。

好像也不是很多嘛,嗯,算法部分就先介紹到這里了,之后的一篇再來說說HashMap中的EntrySet,KeySet和values(如果時間夠的話順便把迭代器也說一說)。
好了,本篇就此愉快的結束了,最后祝大家端午節快樂!如果覺得內容還不錯的話記得動動小手點關注哦,你的支持就是我最大的動力!
