Java HashMap 實現概況及容量
原文連結:https://mp.weixin.qq.com/s/JcnSOGKQlDgaTTFKZFbXnA?scene=25#wechat_redirect
1 簡單說說 HashMap 的底層原理?
當我們往 HashMap 中 put 元素時,先根據 key 的 hash 值得到這個 Entry 元素在陣列中的位置(即下標),然後把這個 Entry 元素放到對應的位置中,如果這個 Entry 元素所在的位子上已經存放有其他元素就在同一個位子上的 Entry 元素以連結串列的形式存放,新加入的放在鏈頭,從 HashMap 中 get Entry 元素時先計算 key 的 hashcode,找到陣列中對應位置的某一 Entry 元素,然後通過 key 的 equals 方法在對應位置的連結串列中找到需要的 Entry 元素,所以 HashMap 的資料結構是陣列和連結串列的結合,此外 HashMap 中 key 和 value 都允許為 null,key 為 null 的鍵值對永遠都放在以 table[0] 為頭結點的連結串列中。
之所以 HashMap 這麼設計的實質是由於陣列儲存區間是連續的,佔用記憶體嚴重,故空間複雜度大,但二分查詢時間複雜度小(O(1)),所以定址容易而插入和刪除困難;而連結串列儲存區間離散,佔用記憶體比較寬鬆,故空間複雜度小,但時間複雜度大(O(N)),所以定址困難而插入和刪除容易;所以就產生了一種新的資料結構叫做雜湊表,雜湊表既滿足資料的查詢方便,同時不佔用太多的內容空間,使用也十分方便,雜湊表有多種不同的實現方法,HashMap 採用的是連結串列的陣列實現方式。
特別說明,對於 JDK 1.8 開始 HashMap 實現原理變成了陣列+連結串列+紅黑樹的結構,陣列連結串列部分基本不變,紅黑樹是為了解決雜湊碰撞後連結串列索引效率的問題,所以在 JDK 1.8 中當連結串列的節點大於 8 個時就會將連結串列變為紅黑樹。區別是 JDK 1.8 以前碰撞節點會在連結串列頭部插入,而 JDK 1.8 開始碰撞節點會在連結串列尾部插入,對於擴容操作後的節點轉移 JDK 1.8 以前轉移前後連結串列順序會倒置,而 JDK 1.8 中依然保持原序。
2 HashMap 預設的初始化長度是多少?為什麼預設長度和擴容後的長度都必須是 2 的冪?
在 JDK 中預設長度是 16(在 Android SDK 中的 HashMap 預設長度為 4),並且預設長度和擴容後的長度都必須是 2 的冪。因為我們可以先看下 HashMap 的 put 方法核心,如下:
-
public V put(K key, V value) {
-
......
-
//計算出 key 的 hash 值
-
int hash = hash(key);
-
//通過 key 的 hash 值和當前動態陣列的長度求當前 key 的 Entry 在陣列中的下標
-
int i = indexFor(hash, table.length);
-
......
-
}
-
-
//最核心的求陣列下標方法
-
static int indexFor(int h, int length) {
-
// assert Integer.bitCount(length) == 1 : "length must be a non-zero power of 2";
-
return h & (length-1);
-
}
可以看到獲取陣列索引的計算方式為 key 的 hash 值按位與運算陣列長度減一,為了說明長度儘量是 2 的冪的作用我們假設執行了 put("android", 123); 語句且 "android" 的 hash 值為 234567,二進位制為 111001010001000111,然後由於 HashMap 預設長度為 16,所以減一後的二進位制為 1111,接著兩數做按位與操作二進位制結果為 111,即十進位制的 7,所以 index 為 7,可以看出這種按位操作比直接取模效率要高。
如果假設 HashMap 預設長度不是 2 的冪,譬如陣列長度為 6,減一的二進位制為 101,與 111001010001000111 按位與操作二進位制 101,此時假設我們通過 put 再放一個 key-value 進來,其 hash 為 111001010001000101,其與 101 按位與操作後的二進位制也為 101,很容易發生雜湊碰撞,這就不符合 index 的均勻分佈了。
通過上面兩個假設例子可以看出 HashMap 的長度為 2 的冪時減一的值的二進位制位數一定全為 1,這樣陣列下標 index 的值完全取決於 key 的 hash 值的後幾位,因此只要存入 HashMap 的 Entry 的 key 的 hashCode 值分佈均勻,HashMap 中陣列 Entry 元素的分部也就儘可能是均勻的(也就避免了 hash 碰撞帶來的效能問題),所以當長度為 2 的冪時不同的 hash 值發生碰撞的概率比較小,這樣就會使得資料在 table 陣列中分佈較均勻,查詢速度也較快。不過即使負載因子和 hash 演算法設計的再合理也免不了雜湊衝突碰撞的情況,一旦出現過多就會影響 HashMap 的效能,所以在 JDK 1.8 中官方對資料結構引入了紅黑樹,當連結串列長度太長(預設超過 8)時連結串列就轉為了紅黑樹,而紅黑樹的增刪改查都比較高效,從而解決了雜湊碰撞帶來的效能問題。