Java:HashMap原理與設計緣由


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 JDK7中新節點為表頭

如果已原有節點為表頭,則需要遍歷鏈表,徒增不必要的性能消耗

3.2 JDK8中新節點為表尾

因為JDK8中鏈表在長度大於等於8時會轉變為紅黑樹,所以每次在鏈表中添加節點,都必須遍歷鏈表計算一次鏈表長度,所以新節點直接在遍歷完鏈表后添加到表尾。

3.3 鏈表過長導致的復雜度問題

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

3.4 還有什么方法解決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值)與高位與運算的結果。

元素在重新計算hash之后,因為n變為2倍,那么n-1的mask范圍在高位多1bit(紅色),因此新的index就會發生這樣的變化:

因此,我們在擴充HashMap的時候,不需要像JDK1.7的實現那樣重新計算hash,只需要看看原來的hash值新增的那個bit是1還是0就好了,是0的話索引沒變,是1的話索引變成“原索引+oldCap”。

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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM