原創|若是懂了HashMap這兩點,面試就沒問題了

HashMap 是後端面試的常客,好比默認初始容量是多少?加載因子是多少?是線程非安全的嗎?put 操做過程複述下?get 操做複述下?在 jdk 1.7 和 1.8 實現上有什麼不一樣?等等一系列問題,可能這些問題你都能對答如流,說明對 HashMap 仍是比較理解的,但最近咱們團隊的同窗作了一個技術分享,其中有幾點我挺有收穫的,我給你們分享下java

咱們每週五都會進行技術分享,你們輪流分享,其實這種機制挺好的,你們坐在一塊兒深刻討論一個知識點,進行思惟的碰撞,多贏面試

拋出兩個問題,看你可否回答出來?

1. 如何找到比設置的初始容量值大的最小的 2 的冪次方整數?算法

2. HashMap 中對 key 作 hash 處理時,作了什麼特殊操做?爲何這麼作?後端

先本身思考下,再往下閱讀效果更佳哦!數組

下面的分析都是針對 jdk 1.8安全

分析

問題1:如何找到比設置的初始容量值大的最小的 2 的冪次方整數?

咱們在用 HashMap 的時候,若是用默認構造器,就會建一個初始容量爲 16,加載因子爲 0.75 的 HashMap。這樣作有個缺點,就是在數據量比較大的時候,會進行頻繁的擴容操做,擴容會發生數據的移位,爲了不擴容,提升性能,咱們習慣預估下容量,而後經過帶容量的構造器建立,看下源碼性能

~~~ javapublic HashMap(int initialCapacity, float loadFactor) {...// 若是設置的初始容量大於最大容量就默認爲最大容量 2^30
if (initialCapacity > MAXIMUM_CAPACITY)initialCapacity = MAXIMUM_CAPACITY;...this.loadFactor = loadFactor;// tableSizeFor 方法主要就是計算比給定的初始容量值大的最小的 2 的冪次方整數this.threshold = tableSizeFor(initialCapacity);}~~~this

經過源碼咱們可知,容量最大值爲 2^30,也就是說 HashMap 的數組部分的長度的範圍爲[0,2^30],而後計算比初始容量大的最小的2的冪次方整數,其中 **tableSizeFor** 方法是重點,咱們看下源碼線程

~~~ java// Returns a power of two size for the given target capacitystatic final int tableSizeFor(int cap) {int n = cap - 1;n |= n >>> 1;n |= n >>> 2;n |= n >>> 4;n |= n >>> 8;n |= n >>> 16;return (n < 0) ? 1 : (n >= MAXIMUMCAPACITY) ? MAXIMUMCAPACITY : n + 1;}~~~設計

這個方法設計的很是巧妙,由於 HashMap 要保證容量是 2 的整數次冪,該方法實現的效果就是若是你輸入的 cap 自己就是 2 的整數次冪,那麼就返回 cap 自己,若是輸入的 cap 不是 2 的整數次冪,返回的就是比 cap 大的最小的 2 的整數次冪

爲何容量要是 2 的整數次冪?

由於獲取 key 在數組中對應的下標是經過 key 的哈希值與數組長度 -1 進行與運算,如:tab[i = (n - 1) & hash]

1. n 爲 2 的整數次冪,這樣 n-1 後以前爲 1 的位後面全是 1,這樣就能保證 (n-1) & hash 後相應的位數既多是 0 又多是 1,這取決於 hash 的值,這樣能保證散列的均勻,同時與運算效率高

2. 若是 n 不是 2 的整數次冪,會形成更多的 hash 衝突

該方法首先執行了 cap -1 操做,這樣作的好處是避免輸入的 cap 是 2 的整數次冪,最後計算的數是 cap 的 2 倍的狀況,由於設置 cap 已經知足 HashMap 的要求了,沒有必要初始化一個 2 倍容量的 HashMap 了,看不明白不急後面有示例分析

前面咱們已經介紹 HashMap 的最大容量爲 2^30,因此容量最大就是 30 bit 的整數,咱們就用 30 位的一個數演示下算法中的移位取或操做,假設 n = 001xxx xxxxxxxx xxxxxxxx xxxxxxxx (x 表明該位上是 0 仍是 1 咱們不關心)

第一次右移 n |= n >>> 1 ,該操做是用 n 自己 和 n 右移 1 位後的數進行或操做,這樣能夠實現把 n 的最高位的 1 緊鄰的右邊一位也置爲 1

~~~n 001xxx xxxxxxxx xxxxxxxx xxxxxxxxn >>> 1 0001xx xxxxxxxx xxxxxxxx xxxxxxxx| 或操做 0011xx xxxxxxxx xxxxxxxx xxxxxxxx結果就是把 n 的最高位爲 1 的緊鄰的右邊的 1 位也置爲了 1,這樣高位中有連續兩位都是 1~~~

第二次右移 n |= n >>> 2

~~~n 0011xx xxxxxxxx xxxxxxxx xxxxxxxxn >>> 2 000011 xxxxxxxx xxxxxxxx xxxxxxxx| 或操做 001111 xxxxxxxx xxxxxxxx xxxxxxxx結果就是 n 的高位中有連續 4 個 1~~~

第三次右移 n |= n >>> 4

~~~n 001111 xxxxxxxx xxxxxxxx xxxxxxxxn >>> 4 000000 1111xxxx xxxxxxxx xxxxxxxx| 或操做 001111 1111xxxx xxxxxxxx xxxxxxxx結果就是 n 的高位中有連續 8 個 1~~~

第四次右移 n |= n >>> 8

~~~n 001111 1111xxxx xxxxxxxx xxxxxxxxn >>> 8 000000 00001111 1111xxxx xxxxxxxx| 或操做 001111 11111111 1111xxxx xxxxxxxx結果就是 n 的高位中有連續 16 個 1~~~

第五次右移 n | n >>> 16

~~~n 001111 11111111 1111xxxx xxxxxxxxn >>> 16 000000 00000000 00001111 11111111| 或操做 001111 11111111 11111111 11111111結果就是 n 的高位1後面都置爲 1~~~

最後會對 n 和最大容量作比較,若是 >= 2^30,就取最大容量,若是 < 2^30 ,就對 n 進行 +1 操做,由於後面位數都爲1,因此 +1 就至關於找比這個數大的最小的 2的整數次冪

011111 11111111 11111111 11111111,這個值就是比給的值大的最小的 2 的整數次冪

下面咱們用一個具體說演示下,好比 cap = 18

cap爲18

咱們輸入的是 18,輸出的是 32,正好是比 18 大的最小的 2 整數次冪

若是 cap 自己就爲 2的整數次冪,輸出結果爲何?cap爲16

經過演示可見,cap 自己就是 2 的整數次冪的輸出結果爲其自己

上面還遺留了個問題,就是先對 cap -1,我解釋說爲了不輸出的是偶數,最後計算的結果爲 2*cap,浪費空間,看下面的演示

cap爲16未減一

經過演示,咱們能夠看出,輸入的是 16,最後計算的結果倒是 32,這就會浪費空間了,因此說算法很牛,先對 cap 作了減一操做

問題2:HashMap 中對 key 作 hash 處理時,作了什麼特殊操做?爲何這麼作?

首先咱們知道 HashMap 在作 put 操做的時候,會先對 key 作 hash 操做,直接定位到源碼位置

~~~javastatic final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}~~~

能夠看到再對 key 作 hash 操做時,執行了 (h = key.hashCode()) ^ (h >>> 16)

~~~原 hashCode 值: 10110101 01001100 10010101 11011111右移 16 位後的值: 00000000 00000000 10110101 01001100異或後的值: 10110101 01001100 00100000 10010011~~~

這個操做是把 key 的 hashCode 值與 hashCode 值右移 16 位作異或(不一樣爲 1,相同爲 0),這樣就是把哈希值的高位和低位一塊兒混合計算,這樣就能使生成的 hash 值更離散

這裏須要我解釋下,經過前面的介紹,咱們知道數組的容量範圍是 [0,2^30],這個數仍是比較大的,平時使用的數組容量仍是比較小的,好比默認的大小 16,假設三個不一樣的 key 生成的 hashCoe 值以下所示:

19305951 00000001 00100110 10010101 11011111

128357855 00000111 10100110 10010101 11011111

38367 00000000 00000000 10010101 11011111

他們三個有個共同點是低 16 位徹底同樣,但高 16 位不一樣,當計算他們在數組中所在的下標時,經過 (n-1)&hash,這裏 n 是 16,n-1=15,15 的二進制表示爲

00000000 00000000 00000000 00001111

用 1930595一、12835785五、38367 都與 15 進行 & 運算,結果以下

hash衝突

經過計算後發現他們的結果同樣,也就是說他們會被放到同一個下標下的鏈表或紅黑樹中,顯然不符合咱們的預期

因此對 hash 與其右移 16 位後的值進行異或操做,而後與 15 作與運算,看 hash 衝突狀況解決hash衝突

可見通過右移 16位後再進行異或操做,而後計算其對應的數組下標後,就被分到了不一樣的桶中,解決了哈希碰撞問題,思想就是把高位和低位混合進行計算,提升分散性

總結

其實 HashMap 還有不少值得研究的點,上面兩個點搞明白後,會感嘆做者寫代碼的能力真是牛,咱們在工做中要借鑑這些思想,但願經過個人講解,你能掌握這兩個知識點,若是有不懂的能夠留言或私聊我

歡迎關注公衆號 【天天曬白牙】,獲取最新文章,咱們一塊兒交流,共同進步!

相關文章
相關標籤/搜索