JDK8中的HashMap相對JDK7中的HashMap做了些優化。
接下來先通過官方的英文注釋探究新HashMap的散列怎么實現
先不給源碼,因為直接看源碼肯定會暈,那么我們先從簡單的概念先講起
(如果你不想深入理解 請不要看括號里的內容,可以簡化閱讀過程)
首先,有一個問題:假如我們現在有一個容量為16的數組,現在我想往里面放對象,我有15個對象。
怎么放進去呢???
其實要解決一個問題就夠了:對象要放在哪個下標???
當然最簡單的方法是從0下標開始一個一個挨着往后放
看,這樣就把你們的對象放滿整個數組了,一個位置也沒有浪費~
但是有17個對象呢?
無論無何必須有兩個對象在同一個槽位(槽位指的是數組中某個下標的空間)了,如果不擴充數組的大小的話
那我們采取的策略最簡單的是像上面一樣先塞滿數組,最后一個對象隨機放到一個位置,用鏈表的形式把他掛在數組中某個位置的對象上。
(較新版本的JDK中 如果鏈表太長會變成樹)
但是如果現在我們有20個對象呢???50個對象呢???100個,1000個對象呢???
每個槽位需要承受的對象數量會越來越多,如果只是一味地掛對象,而不采取合適的策略確定要加上去的對象到底放在哪個位置的話,很有可能出現下面這種狀況。
那么當我們查找一個對象的時候可能遇到這種情況,
這樣的話,查詢效率十分低下,我們希望加上去的對象在整個數組上呈均勻分布的趨勢,這樣就不會出現某個槽承受了很多對象但是有的槽位承受很少對象,甚至只有一個對象的情況。
下面是我們希望的結果。
因為要查詢的話最多查兩次就能查到我們想要的對象了。
這樣我們就不得不決定,要加入的對象在數組的下標了!
怎么確定下標呢?有一種確定下標的方法,這種確定下標的方法(算法)叫做散列。很形象吧,打散,列開。
散列的過程就是通過對象的特征,確定他應該放在哪個下標的過程。
那這個特征是什么呢???
哈希碼!(hashCode的翻譯)
java每個對象都有一個叫"hashCode"的標簽碼 和他對應,當然這個hashCode不一定是唯一的。
(在HashMap的源碼中調用了key.hashCode()來獲得hashCode,請注意,因為實際調用到的是運行時對象所屬類的方法
[比如類A繼承了類Object,A重寫了Object的hashCode()方法,Object ob = new A(); ,ob.hashCode();調用的實際是類A重寫后的hashCode方法
所以我們可以通過重寫 hashCode() 方法來返回我們想要的hashCode值]
所以不同對象的hashCode 可能是一樣的,取決於類怎么重寫hashCode()
)
我們的問題可以簡化為,怎么把我們的hashCode映射到下標的二進制碼上呢?
現在假設我們的 hashCode 是8位的 (實際上是32位的),比如下面就是一個對象a 的hashCode
假如我們的數組大小是16,那么我們要根據hashCode 確定好數組下標,下標的范圍是0~15.
該怎么確定呢?我們可以用直接映射的方法
我們發現,把hashCode 的二進制碼直接映射到數組下標的二進制碼上,直接把高位全部置為0,好像可以喔。
而且 因為我們用低四位去映射,所以范圍會保持在0~15間,所以最后映射的結果總是沒有超出范圍
這樣的話,上圖的hashCode 的數組下標就是 7( 1 + 2 + 4 = 7, 0111的十進制=7)
但是,進一步觀察,我們發現,無論高位怎么樣,只要低位相同,都會映射到同一個數組下標上。
高位有 2 ^ 4 = 16 種情況,這16種情況都會瞄准同一個數組下標,何況實際上我們的hashCode是32位的,這樣的話就有 2 ^ (32 - 4) = 2 ^ 28 種沖突
出現了我們之前擔心的場景,許多甚至所有對象組成一條鏈表掛在一個位置上,這樣查詢效率十分低下。
這種對不同對象進行散列,但是最后得到的下標相同的情況稱為hash沖突,也可以稱為散列沖突,其實散列就是hash翻譯過來的。
好的,正片開始!
我們來看看JDK8中的HashMap是怎么解決這種沖突的。
首先我們要知道,JDK8是怎么執行散列的
JDK8使用了掩碼,即是下文注釋中將提到的用來masking的數值
這個掩碼是根據HashMap存儲對象的數組的大小決定的,圖中table就是我們所說的hash表,n - 1 被作為掩碼和 傳進來的hash值(也就是hashCode)
進行 & 運算。
看下面一個例子更明了一點。
比如大小為 32 的hash表
32的二進制數是 0010 0000
那么32 - 1 = 31 就是 0001 1111
0001 1111 & A 會得到什么呢,0001 1111 像一塊掩布一樣,將和他 & 的數 A 的前三位都遮住,全部變成0,其他位不變,所以被稱為掩碼。
比如 A = 1101 0101
因為我們的掩碼前三位全是0 那么他就會把A的前三位全部掩蓋掉,掩碼后面的1,和A對應位 & 之后保持不變
現在再來看看官方源碼的hashCode是怎么減少沖突的。
來看hash 方法上的一段注解, hash方法是把hashCode再散列一次,把散列hashCode后的值作為返回值返回,以此再次減少沖突,而過程是把高位的特征性傳到低位。
每個 [] 中的內容都是對前面一小段的解釋,如果嫌麻煩可以直接讀解釋,不讀英文
/**
* Computes key.hashCode() [計算得出hashCode 不歸hash函數管] and spreads (XORs) higher bits of hash
* to lower[把高位二進制序列(比如 0110 0111 中的 0110) , 的特征性傳播到低位中,通過異或運算實現]. Because the table uses power-of-two masking[HashMap存儲對象的數組容量經常是2的次方,這個二的次方(比如上面是16 = 2 ^ 4) 減1后作為掩碼], sets of
* hashes that vary only in bits above the current mask will
* always collide[在掩碼是2^n - 1 的情況下,只用低位的話經常發生hash沖突,見上述例子]. (Among known examples are sets of Float keys
* holding consecutive whole numbers in small tables.) So we
* apply a transform that spreads the impact of higher bits
* downward[將高位的特征性傳播到低位去]. There is a tradeoff between speed, utility, and
* quality of bit-spreading[但是這種特性的傳播會帶來一定的性能損失]. Because many common sets of hashes
* are already reasonably distributed (so don't benefit from
* spreading)[因為有的hashCode他們的低位已經足夠避免多數hash沖突了,比如我們的hashCode是八位的
並且我們的數組大小是((2 ^ 8) - 1) (0111 1111) 那么只有兩種沖突情況而已,0mmm mmmm 和 1mmm mmmm 會沖突,每次進行插入元素或者查找元素都要調用hash函數再一次散列hashCode,顯然不划算], and because we use trees to handle large sets of
* collisions in bins[其實之前說的若干對象變成鏈表掛在一個數組位置上,已經是一種解決沖突的辦法了], we just XOR some shifted bits in the
* cheapest possible way to reduce systematic lossage[所以我們只用最簡單的異或運算來減少沖突,減少性能損失], as well as
* to incorporate impact of the highest bits that would otherwise
* never be used in index calculations because of table bounds[把本來可能因為數組大小限制而用不上(上面說的就算高位不同,只要低位相同就可以指向同一個數組下標),的高位也用上].
*/
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
什么意思呢?什么叫做把高位的特征也用上?
比如我們之前說的。當我們有一個大小為16的數組,下面是兩個對象的hashCode
0110 0111
1100 0111
如果我們直接用這兩個未經hash函數處理的hashCode 通過JDK的方法得出下標:
n = 16
16 = 0001 0000
16 - 1 = 15 = 0000 1111
hash(這是上圖藍字變量) = hashCode(未經hash函數再散列)
0110 0111 & 0000 1111 = 0000 0111 ------ 7
1100 0111 & 0000 1111 = 0000 0111 ------ 7
求得同一個下標,顯然沖突了,就算兩個hashCode他們的高位不同,但還是會沖突
現在我們用上高位的特性,
因為本來hashCode是32位的,所以上面 >>> 的是16,也就是高一半的位移到低一半去
而我們設置的hashCode 是8位的,所以上面的 >>> 的應該是 4
hash (上面藍字變量) = hash (hashCode) ------ hash函數對hashCode 再散列
對應過程如下圖
正如我們所見,原本沖突的低四位,把高位的特征傳到他們上面后,他們不沖突了!
當我們對這些再散列后的結果用掩碼掩掉不必要的高位之后(見上面的紅框框圖)(比如高四位),剩下的是
0000 1011
0000 0001
對應的數組下標是 11 和 1
解決了沖突!
關於HashMap的擴容篇正在路上~