作過 java 開發的朋友們相信都很熟悉 HashMap 這個類,它是一個基於 hashing 原理用於存儲 Key-Value 鍵值對的集合,其中的每個鍵也叫作 Entry
,這些鍵分別存儲在一個數組當中,系統會根據 hash
方法來計算出 Key-Value 的存儲位置,能夠經過 key 快速存取 value。
HashMap 基於 hashing 原理,當咱們將一個鍵值對(Key-Value) 傳入 put
方法時,它將調用這個 key 的 hashcode
方法計算出 key 的 hashcode 值,而後根據這個 hashcode 值來定位其存放數組的位置來存儲對象(HashMap 使用鏈表來解決碰撞問題,當其發生碰撞了,對象將會存儲在鏈表的下一個節點中,在鏈表的每一個節點中存儲 Entry
對象,在 JDK 1.8+ 中,當鏈表的節點個數超過必定值時會轉爲紅黑樹來進行存儲),當經過 get
方法傳入一個 key 來獲取其對應的值時,也是先經過 key 的 hashcode
方法來定位其存儲在數組的位置,而後經過鍵對象的 eqauls
方法找到對應的 value 值。接下來讓咱們看看其內部的一些實現細節。(PS:如下代碼分析都是基於 JDK 1.8
)java
由於獲取 key 在數組中對應的下標是經過 key 的哈希值與數組的長度減一進行與運算來肯定的(tab[(n - 1) & hash]
)。當數組的長度 n 爲 2 的整數次冪,這樣進行 n - 1 運算後,以前爲 1 的位後面全是 1 ,這樣就能保證 (n - 1) & hash
後相應位的值既多是 1 又多是 0 ,這徹底取決於 key 的哈希值,這樣就能保證散列的均勻,同時與運算(位運算)效率高。若是數組的長度 n 不是 2 的整數次冪,會形成更多的 hash 衝突。HashMap 提供了以下四個重載的構造方法來知足不一樣的使用場景:算法
HashMap()
,使用該方法表示所有使用 HashMap 的默認配置參數HashMap(int initialCapacity)
,在初始化 HashMap 時指定其容量大小HashMap(int initialCapacity, float loadFactor)
,使用自定義初始化容量和擴容因子Map
來構造 HashMap:HashMap(Map<? extends K, ? extends V> m)
,使用默認的擴容因子,其容量大小有傳入的 Map
大小來決定前三個構造方法最終都是調用第三個即自定義容量初始值和擴容因子構造 HashMap(int initialCapacity, float loadFactor)
,其源碼實現以下數組
public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); }
從源碼實現能夠看出,若是咱們傳入的初始容量值大於 MAXIMUM_CAPACITY
時,就設置容量爲 MAXIMUM_CAPACITY
,其值以下:this
/** * The maximum capacity, used if a higher value is implicitly specified * by either of the constructors with arguments. * MUST be a power of two <= 1<<30. */ static final int MAXIMUM_CAPACITY = 1 << 30;
也就是容量的最大值爲 2 的 30 次方(1 << 30
)。咱們知道,HashMap 的容量始終是 2 的整數次冪,無論咱們傳入的初始容量是什麼,它都會使用最接近這個值而且是 2 的整數次冪做爲 HashMap 的初始容量,這一步處理是經過 tableSizeFor
方法來實現的,咱們看看它的源碼:spa
經過方法的註釋咱們也能夠知道(英語對於從事技術開發的人過重要了~~~
),此方法的返回值始終是 2 的整數次冪,它是如何作到的呢?接下來咱們經過一個例子一步一步來看,假設咱們傳入的初始容量大小 cap
的值 cap
爲 15。設計
第 ① 步:將 cap - 1
後,n
的值爲 14(15 - 1)。 3d
第 ② 步:將 n
的值先右移 1 位後與 n
進行 或預算
(二者都爲 0 結果爲 0,其它狀況都爲 1),下面是具體的計算過程:code
第 ③ 步:將 n
的值先右移 2 位後與 n
進行 或預算
(二者都爲 0 結果爲 0,其它狀況都爲 1),下面是具體的計算過程:對象
第 ④ 步:將 n
的值先右移 4 位後與 n
進行 或預算
(二者都爲 0 結果爲 0,其它狀況都爲 1),下面是具體的計算過程:blog
第 ⑤ 步:將 n
的值先右移 8 位後與 n
進行 或預算
(二者都爲 0 結果爲 0,其它狀況都爲 1),下面是具體的計算過程:
第 ⑥ 步:將 n
的值先右移 16 位後與 n
進行 或預算
(二者都爲 0 結果爲 0,其它狀況都爲 1),下面是具體的計算過程:
最後若是 n
的值小於 0
,則返回 1,若是大於最大值 MAXIMUM_CAPACITY
則返回 MAXIMUM_CAPACITY
,不然返回 n + 1
。 如今 n 爲 15,因此返回 n + 1(16),而 16 正好是 2 的 4 次冪。有的朋友可能會問,剛剛上文假設的初始容量大小 cap
是 15,原本就不是 2 的整數次冪,若是我傳入初始容量的就是 2 的整數次冪那會怎麼樣呢?如今假設傳的的初始容量大小的 32(2 的 5 次方)看看結果是什麼。
第 ① 步:將 cap - 1
後,n
的值爲 31(32 - 1)。
第 ② 步:將 n
的值先右移 1 位後與 n
進行 或預算
(二者都爲 0 結果爲 0,其它狀況都爲 1),下面是具體的計算過程:
第 ③ 步:將 n
的值先右移 2 位後與 n
進行 或預算
(二者都爲 0 結果爲 0,其它狀況都爲 1),下面是具體的計算過程:
第 ④ 步:將 n
的值先右移 4 位後與 n
進行 或預算
(二者都爲 0 結果爲 0,其它狀況都爲 1),下面是具體的計算過程:
第 ⑤ 步:將 n
的值先右移 8 位後與 n
進行 或預算
(二者都爲 0 結果爲 0,其它狀況都爲 1),下面是具體的計算過程:
第 ⑥ 步:將 n
的值先右移 16 位後與 n
進行 或預算
(二者都爲 0 結果爲 0,其它狀況都爲 1),下面是具體的計算過程:
通過以上 6 步計算後得出 n 的值爲 31,大於 0 小於 MAXIMUM_CAPACITY
返回 n + 1
,因此通過計算後的初始容量大小的 32。稍微總結一下,咱們能夠得出:若是咱們傳入的初始容量大小不是 2 的整數次冪,那麼通過計算後的初始容量大小爲大於咱們傳入初始容量值的最小值而且是 2 的整數次冪。細心的朋友會發現,爲何第一步要進行 cap - 1
的操做呢?那是由於,若是不進行 - 1 運算的話,當咱們傳入的初始容量大小爲 2 的整數次冪的時候,經過以上步驟計算出來的結果值爲傳入值的 2 倍。假設咱們傳入的初始容量大小爲 32,此時沒有第 ① 步(cap - 1
)的操做,那麼依次經過以上 ②、③、④、⑤、⑥ 後爲 63
,最後再進行 n + 1
操做,結果爲 64
是 傳入值 32 的 2 倍,顯然和預期結果(32
)不符。這個計算初始容量的算法仍是很巧妙的,先進行了 -1 的操做,保證傳入初始容量值爲 2 的整數次冪的時候,返回傳入的原始值。
不論是經過 get
方法獲取 key 對應的 Value 值或者經過 put
方法存儲 Key-Value 鍵值對時,都會先根據 key 的哈希值定位到數組的位置,咱們看看 HashMap 裏的 hash
方法是如何實現的,源碼以下:
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
當 key 爲 null
時,返回 0,不然進行 h = key.hashCode()) ^ (h >>> 16
運算,先調用 key 的 hashCode
方法獲取 key 的哈希值,而後與 key 的哈希值右移 16 位後的值進行異或運算(相同爲 0,不一樣爲 1,簡稱 同假異真
),爲何獲取 key 的哈希值還要再進行異或運算,直接返回 key 的哈希值好像也沒什麼問題,若是沒有後面的異或運算,直接返回哈希值,咱們假設數組的長度爲 16,如今要往 HashMap 存入的三個鍵值對的 key 的哈希值分別爲 3283一、3355449五、2097215,根據 hash 方法返回值定位到數組的位置((n - 1) & hash
),以上三個值和 15(16 - 1)進行 & 運算(都爲 1 才爲 1,其它狀況都爲 0)
以下:
能夠發現以上三個哈希值都定位的數組下標爲 15 的位置上。因此 hash
若是方法沒有後面與哈希值右移 16 位後的值進行異或運算的話,當數組長度比較小時很容易形成 哈希碰撞
,即多個 key(不一樣的哈希值)都會定位到數組上的同一個位置,也就是說會放入到同一個鏈表或者紅黑樹中,由於此時 key 的哈希值只有低位的纔會參與運算,顯然和咱們的預期不符合。可見 hash
方法將 key 的哈希值與其右移 16 位後進行異或運算能減小哈希碰撞的次數,把高位和低位都參與了運算,提升了分散性。
HashMap 其實還有不少值得咱們深刻研究的點,看懂了上面兩個方法後,不得不佩服做者的代碼設計能力,JDK 中有不少優秀源碼都值得咱們好好品味,看代碼的時候必定要多看幾遍多問幾個爲何,特別是經典的源代碼,而後將這些思想運用到咱們的實際工做中。