HashMap哲學中的數學原理

今天與羣友暢談HashMap的知識點,十分愉快。但恐於泛泛之言,寥寥數語難以概述清晰。幸甚至哉,做文詠志。
此文並不會講解HashMap的數據結構。單單闡述HashMap涉及到的數學原理。java

tableSizeFor

通常遇到的第一個問題就是tableSizeFor()。算法

我來解釋一下這部分代碼的做用: 計算出大於或等於cap的第一個2的n次冪。 注意:

  • cap參與計算以前要先-1
  • 瞭解>>>、|的運算規則

他這個算法大概的意思就是先無符號右移m位,再將得到的結果與原值進行 | 運算。 好比咱們令cap=19,則n=18,二進制表示爲10010,無符號右移1位以後是1001。 若是將其對其爲16位則表示爲:0000 0000 0001 0010而後與原值0000 0000 0000 1001與運算結果爲:0000 0000 0001 1011後面的幾輪咱們以此類推便可。
詳細的演算過程以下:數組

上面的演算結果是11111,翻譯爲10進制就是31。

return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
複製代碼

n < MAXIMUM_CAPACITY,因此返回:31 + 1 = 32。
使用 移位 和 或 運算,巧妙的化簡了問題。注意這裏有一個細節就是cap-1,爲何這麼作呢。假設 x = 2^n,若是咱們不處理獲得的結果是2 * 2^n,這裏就不符合要求。大於等於 x 的第一個值應該是 x 自己。不信的話,能夠動手驗證一下。數據結構

hash

這裏咱們不討論hashCode產生的過程,咱們只關心後部分。也就是你們所謂的擾動函數。函數

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : 
    // 咱們看最後一行做用
    (h = key.hashCode()) ^ (h >>> 16);
}
複製代碼

Jdk對此作出了相應解釋。性能

Computes key.hashCode() and spreads (XORs) higher bits of hashspa

直譯就是:計算key.hashCode()並擴展哈希的更高位
有必要思考一下:爲何已經得出key的hashCode以後還要大費周章進行 移位 異或的運算呢。假設咱們不進行擾動呢,能有什麼影響呢。咱們知道HashMap中肯定元素所在數組中的位置是經過 & 運算來實現的。翻譯

if ((p = tab[i = (n - 1) & hash]) == null)
複製代碼

一個key的hashCode足夠大,而當前HashMap的容量不是足夠大的時候,你發現了嗎對該key進行定位的重大決定其實只是交給了最後幾位。
假設當前容量是16,而後(n - 1) = 15,其實就是二進制的1111。不管hash有多少位,不足的我1111只管補足0便可。這時候一個很是尷尬的狀況出現了。我管你hashCode多少位只要跟我1111進行 & 運算,除了最後四位前面的一概爲0。也就是說hash看着位數足夠多其實發揮做用的只有最後面的一部分。這樣一來,兩個hash只須要低位一致,高位就算多大差別他們最後必定定位再同一處
這顯然是源碼做者不肯意看到的結果。其實這裏高位參與運算就是爲了打亂低位。這樣高位不一樣,低位相同,定位就必定相同的僵局直接就會被打破。code

容量的祕密

咱們知道HshMap擴容至原來的2倍,初始容量是16,這麼一來ta的容量其實永遠都是2^n。在二進制中2^n的數字是極具特色的。轉化成二進制以後首位爲1,餘位爲0。那麼2^n - 1以後呢,獲得的結果更加有規律,必定獲得所有爲 1 的排列。
請你必定要記得任何數 -1 獲得所有是 1 的排列那麼這個數字必定是2^ncdn

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) n = (tab = resize()).length;
    (n - 1) & hash
複製代碼

在put()方法中想要定位,其實就是獲取到Key的hash以後對當前容量求餘。可是求餘在計算機的世界裏比較消耗性能。咱們能夠將hash % n轉化爲(n - 1) & hash
& 的運算規則是所有爲 1 結果爲 1,其他結果爲 0。
咱們令hash的值爲a b c d,(n - 1)的值爲m n x y,且上述變量取值範圍爲[0, 1]
& 運算結果爲t x y z,最完美的狀況是: 四位數值每一位均可能是 0 或者 1,那麼獲得的排列有2 * 2 * 2 * 2 = 16 種。咱們繼續假設若是m n x y其中有一個數字等於 0 呢?結果的排列就只剩下2 * 2 * 2 = 8種。出現兩位爲 0 的話排列就只剩下2 * 2 = 4種。
這麼理論難免有些枯燥。由於咱們是明確知道當前容量的。若是 n = 1110,n - 1 = 1101,至關於你數組長度爲 14 時,只會出現 8 種排列,那麼對應成HashMap的定位問題,就是說數組長度是 14 時,足足有 6 個位置是永遠浪費的。若是存在這種大量浪費的狀況,勢必會致使HashMap頻頻擴容,損耗性能。
那怎麼解決呢?怎麼讓全部的位置都有可能被定位,對應成上述數學模型解決方案其實就是(n - 1)全部位必須全爲 1。
那若是(n - 1)全部位必須全爲 1。則 n = 2^x 成立。

擴容的祕密

if ((e.hash & oldCap) == 0)
newTab[j] = loHead;
newTab[j + oldCap] = hiHead;
複製代碼

這裏我當時看的時候百思不得其解,直到我發現這裏(e.hash & oldCap)不是(e.hash & (oldCap - 1))這二者天壤之別。這句代碼這裏的意思就是其實就是判斷oldCap最高位對應e.hash相對應位置上的值是否爲 1。
這個很重要由於若是對應的位置爲 1,直接表示ta須要搬家,而搬到哪裏去呢?數組原來位置對應的數字加上原容量便可。若是是 0,那就待在原地便可。其實這裏只要二進制和數學學的好的,應該是瞬間就能夠反應的過來。
擴容以後(e.hash & (newCap - 1))其實只有newCap的最高位對應的e.hash的那一位能夠發揮做用,newCap最高位前面的全是0不影響結果,後面呢跟(e.hash & (oldCap - 1))的結果又一摸同樣也不影響結果。還不明白,那我就偷一張圖:

要是還不明白咱就代數演示一下:
假設原容量 n=10000,n - 1 = 1111
假設 key.hash = 10001
那麼ta所在的位置是 1
而後擴容一下
如今 n=100000,n - 1 = 11111
那麼ta所在的位置是 10001

原諒我,標題致敬了牛頓一番......

相關文章
相關標籤/搜索