Java:HashMap原理與設計緣由

EricTao2發表於2019-07-04

Java:HashMap原理與設計緣由

前言

Java中使用最多的資料結構基本就是ArrayList和HashMap,HashMap的原理也常常出現在各種面試題中,本文就HashMap的設計與設計緣由作出一一講解,並解答面試常見的一些問題。

一 HashMap資料結構

HashMap是一張雜湊表(即陣列),表中的每個元素都是鍵值對(Map.Entry類)。並且每個元素都是一個連結串列(紅黑樹)的節點。並且HashMap的陣列長度一定是2的次冪

1.1 為何陣列長度一定是2的次冪

正常情況下,新增節點時,會對節點進行取模運算,確定節點在雜湊表中的位置。但是當雜湊表(陣列)長度為2的次冪時,取模運算可以修改為位與運算
原始碼如下:

static final int hash(Object key) {
    if (key == null){
        return 0;
    }
    int h;
    h = key.hashCode();返回雜湊值也就是hashcode
    // ^ :按位異或
    // >>>:無符號右移,忽略符號位,空位都以0補齊
    //其中n是陣列的長度,即Map的陣列部分初始化長度
    return (n-1)&(h ^ (h >>> 16));
}

具體原理可以參考專門講解該演算法的文章:
由HashMap雜湊演算法引出的求餘%和與運算&轉換問題

二 HashMap的鍵值儲存

我們給 put() 方法傳遞鍵和值時,我們先對鍵呼叫 hashCode() 方法,計算並返回 hashCode,然後使用HashMap內部的hash演算法,將hashCode計算為表中的具體位置,找到 Map 陣列的 bucket 位置來儲存 Node 物件。

三 解決Hash碰撞

使用拉鍊法

如果hash到的陣列位置已存在物件,即為Hash碰撞。JDK使用拉鍊法解決Hash碰撞問題。
即以原有的Node節點為基礎,構造連結串列。將新的Node節點設為連結串列表頭。

3.1 為何新節點為表頭

如果已原有節點為表頭,則需要遍歷連結串列,徒增不必要的效能消耗

3.2 連結串列過長導致的複雜度問題

HashMap的查詢操作最佳時間複雜度是O(1),但是當表中的某個連結串列過長時,查詢該連結串列上的元素時間複雜度為O(n)JDK1.8中解決了該問題,當HashMap中某連結串列長度大於8時,連結串列會重構為紅黑樹,這樣,HashMap的最壞時間複雜度為O(n)。同理,為了不必要的消耗,當連結串列長度小於6時,紅黑樹會重新變回連結串列

3.3 還有什麼方法解決Hash碰撞

開放定址法,再雜湊法
感興趣可以參看此文:
Hash碰撞和解決策略

四 HashMap的擴容

4.1 擴容時機

當size超過閾值(**陣列長度*負載因子**)時,即開始擴容,HashMap的負載因子為0.75。

4.1.1 為何要陣列未滿就擴容

避免頻繁出現Hash碰撞,造成拉鍊過長(紅黑樹過長)。這樣會導致查詢複雜度頻繁出現最壞情況

4.2 擴容過程

建立原本陣列容量*2的新陣列,將節點從原本的陣列中遷移過去。

4.2.1 為何擴容的倍數是2倍

原因一上文已說明,方便進行雜湊運算。
原因二是不需要重新計算Hash值(JDK1.8優化)。經過觀測可以發現,我們使用的是2次冪的擴充套件(指長度擴為原來2倍),所以,經過rehash之後,元素的位置要麼是在原位置,要麼是在原位置再移動2次冪的位置。對應的就是下方的resize的註釋。

/** 
 * Initializes or doubles table size.  If null, allocates in 
 * accord with initial capacity target held in field threshold. 
 * Otherwise, because we are using power-of-two expansion, the 
 * elements from each bin must either stay at same index, or move 
 * with a power of two offset in the new table. 
 * 
 * @return the table 
 */  
final Node<K,V>[] resize() {  }

看下圖可以明白這句話的意思,n為table的長度,圖(a)表示擴容前的key1和key2兩種key確定索引位置的示例,圖(b)表示擴容後key1和key2兩種key確定索引位置的示例,其中hash1是key1對應的雜湊值(也就是根據key1算出來的hashcode值)與高位與運算的結果。
Java:HashMap原理與設計緣由
元素在重新計算hash之後,因為n變為2倍,那麼n-1的mask範圍在高位多1bit(紅色),因此新的index就會發生這樣的變化:
Java:HashMap原理與設計緣由
因此,我們在擴充HashMap的時候,不需要像JDK1.7的實現那樣重新計算hash,只需要看看原來的hash值新增的那個bit是1還是0就好了,是0的話索引沒變,是1的話索引變成“原索引+oldCap”。

五 重寫equals方法需同時重寫hashCode方法

這個是老生常談的問題了,如果順利理解了HashMap的底層結構那麼這個問題就很好理解了。equals相同的key理論上必定有相同hashCode,所以必須也重寫hashCode方法。可以思考下如果沒重寫,在put,get過程中會導致什麼問題。

相關文章