Java:HashMap原理與設計原因

前言

Java中使用最多的數據結構基本就是ArrayList和HashMap,HashMap的原理也經常出如今各類面試題中,本文就HashMap的設計與設計原因做出一一講解,並解答面試常見的一些問題。html

一 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碰撞

使用拉鍊法post

若是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碰撞和解決策略3d

四 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過程當中會致使什麼問題。

相關文章
相關標籤/搜索