HashMap源碼分析

在看本文之前财异,強(qiáng)烈建議去讀下我的上一篇文章HashMap的hash機(jī)制詳解 劳跃,有了這個(gè)基礎(chǔ)后本文才更容易理解。

在分析源碼之前邑闲,這里對(duì)整個(gè)HashMap機(jī)制大致做下介紹算行,HashMap還是基于hash表的數(shù)據(jù)結(jié)構(gòu),解決hash碰撞用的也是拉鏈法监憎,不同的是纱意,java8不只是一個(gè)簡(jiǎn)單的鏈表了婶溯,鏈表長度如果過長會(huì)變成紅黑樹鲸阔。而且和普通固定大小的hash表不同,HashMap需要?jiǎng)討B(tài)擴(kuò)容迄委。所以我們閱讀源碼時(shí)要重點(diǎn)關(guān)注:如何初始化褐筛,何時(shí)擴(kuò)容,擴(kuò)容時(shí)要考慮哪些叙身,解決hash碰撞時(shí)何時(shí)變成紅黑樹渔扎,何時(shí)又會(huì)重新變成鏈表等等。

特殊屬性

首先看下HashMap有哪些關(guān)鍵屬性:

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {
    /**
     * 數(shù)組初始化大小為16
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    /**
     *數(shù)組最大容量
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;
    /**
     * 擴(kuò)容因子信轿,如果目前數(shù)組已使用空間占用總空間的比例達(dá)到或超過這個(gè)值就要擴(kuò)容
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    /**
     *鏈表的長度超過這個(gè)值就會(huì)轉(zhuǎn)換成紅黑樹
     */
    static final int TREEIFY_THRESHOLD = 8;
    /**
     *紅黑樹的節(jié)點(diǎn)數(shù)量小于或等于這個(gè)值就要退化到鏈表
     */
    static final int UNTREEIFY_THRESHOLD = 6;
    /**
     * The smallest table capacity for which bins may be treeified.
     * (Otherwise the table is resized if too many nodes in a bin.)
     * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
     * between resizing and treeification thresholds.
     */
    static final int MIN_TREEIFY_CAPACITY = 64;
 
    //真正存儲(chǔ)的數(shù)據(jù)結(jié)構(gòu)
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
    }
    //Hash表真正存儲(chǔ)數(shù)據(jù)的數(shù)組
    transient Node<K,V>[] table;
   //如果總節(jié)點(diǎn)數(shù)量達(dá)到這個(gè)值就要擴(kuò)容
    int threshold;
}

初始化

public HashMap(int initialCapacity, float loadFactor) {
       //此處省略一堆檢查校驗(yàn)
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }
    public HashMap(int initialCapacity) {
        //初始大小默認(rèn)16晃痴,擴(kuò)容因子為0.75
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    public HashMap() {
        //默認(rèn)0.75f
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

可以看到整個(gè)初始化其實(shí)就是確定兩個(gè)值,初始容量以及擴(kuò)容因子,另外計(jì)算了真正需要擴(kuò)容的那個(gè)閾值:threshhold
這里大家注意财忽,沒有對(duì)數(shù)組table進(jìn)行任何實(shí)例化操作倘核,沒有真正申請(qǐng)任何空間。

增加(碰撞解決)

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

這里首先是通過hash方法重新計(jì)算了key的hash值:

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

至于這個(gè)方法為什么這么寫即彪,可以參考我的上一篇文章:HashMap的hash機(jī)制詳解
然后就是真正的putVal()方法紧唱,由于方法本身比較長,這里不一次性貼出整個(gè)方法源碼,我們一段一段來分析:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //省略其他代碼
}

這里首先判斷了當(dāng)前table數(shù)組是否為空漏益,空了要去開始擴(kuò)容蛹锰,之所以要做這個(gè)判斷是因?yàn)?上一步初始化時(shí),HashMap并沒有真正申請(qǐng)任何空間绰疤,所以這里是利用了擴(kuò)容方法resize()順帶初始化table數(shù)組了铜犬。

 */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
       //省略代碼
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
       //省略代碼
}

這里大家注意i=(n-1)&hash的計(jì)算,這個(gè)就是根據(jù)hash值來確定當(dāng)前value放置在table數(shù)組的哪個(gè)位置上轻庆,為什么這么算還是參考我的上篇文章:HashMap的hash機(jī)制詳解翎苫。先判斷目標(biāo)位置有沒有被之前的元素占用,如果沒有直接放到該位置上榨了,否者就是下面要開始處理hash碰撞了煎谍。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //此處省略代碼
        else {
            Node<K,V> e; K k;
            //此處的p就是產(chǎn)生hash碰撞位置的原有的數(shù)據(jù),e用來存放被覆蓋的數(shù)據(jù)
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))   // 1
                e = p;    
            else if (p instanceof TreeNode)   // 2
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {   //  3
                //此處開始遍歷鏈表
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null); //4
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st    
                            //5
                            treeifyBin(tab, hash);
                        break;
                    }
                  
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))   //6
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            } 
        }
       //此處省略代碼

hash碰撞頁分成好幾種情況龙屉,

  • key完全相同 呐粘,這就是1號(hào)if對(duì)應(yīng)的情況,key完全相同時(shí)转捕,這里直接記錄了原來的值作岖,這里并沒有直接覆蓋
  • key不同,但hash值相同五芝,而且由于此處hash碰撞次數(shù)太多痘儡,處理碰撞的鏈表已經(jīng)變成了紅黑樹,這是2號(hào)if對(duì)應(yīng)的情況枢步,這里直接將新的值插入到紅黑樹即可
  • key不同沉删,但hash值相同,而且目前處理hash碰撞還是鏈表形態(tài)醉途,這里對(duì)應(yīng)3好else情況矾瑰,如果順利的話直接遍歷鏈表到尾節(jié)點(diǎn),然后插入新的值(情況4)隘擎,插入新值后如果鏈表長度達(dá)到了TREEIFY_THRESHOLD(8)就會(huì)開始轉(zhuǎn)變成紅黑樹(情況5)殴穴,以上是理想情況,但還有一種情況货葬,如果和之前鏈表中某個(gè)節(jié)點(diǎn)的key相同呢(情況6)采幌? 這個(gè)時(shí)候就不用繼續(xù)遍歷鏈表了,此時(shí)的e就指向了那個(gè)相同key的節(jié)點(diǎn)震桶。

上面我們處理了hash碰撞中key不相同的情況休傍,現(xiàn)在看看key相同怎么處理的:

            //此處的e就是key相同時(shí)的舊值,會(huì)被新的value替代
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }

可以看到?jīng)]有什么操作尼夺,只是在允許(HashMap可以設(shè)置不覆蓋同key的舊值)的情況下直接覆蓋舊值尊残,并返回新值炒瘸。
現(xiàn)在插入完成了,我們還需要判斷是否需要擴(kuò)容:

        if (++size > threshold)
            resize();

這里注意下寝衫,如果是同key的話顷扩,hashmap并沒有使用新的空間,只是覆蓋而已慰毅,上面已經(jīng)直接return oldValue了隘截,只有在插入了新的值的時(shí)候才會(huì)判斷擴(kuò)容。

擴(kuò)容

在分析源碼之前汹胃,大家先思考下:擴(kuò)容到底擴(kuò)的是哪部分婶芭?擴(kuò)容時(shí)如果是table+鏈表的結(jié)構(gòu),擴(kuò)容后鏈表怎么辦着饥?如果是紅黑樹呢犀农?紅黑樹怎么辦?

確定要擴(kuò)多大

        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {   // 1
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) //  2   initial capacity was placed in threshold
            newCap = oldThr;
        else {               // 3     zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;

情況2是初始化時(shí)指定了threshold宰掉,這個(gè)情況比較少呵哨,我們重點(diǎn)關(guān)注情況3和情況1。

  • 第一次擴(kuò)容(情況3)
    之前初始化時(shí)我們還記得那時(shí)只是簡(jiǎn)單的確定了初始容量和擴(kuò)容因子轨奄,具體空間沒有申請(qǐng)孟害,我們第一次put的時(shí)候就會(huì)觸發(fā)這個(gè)擴(kuò)容機(jī)制,進(jìn)入到情況3中來挪拟。
  • 后續(xù)擴(kuò)容(情況1)
    后續(xù)正常擴(kuò)容一般都會(huì)進(jìn)入情況1挨务,超過最大MAXIMUM_CAPAXITY的情況也比較少,所以一般都會(huì)走
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1;

這樣容量翻倍玉组,擴(kuò)容threshold也翻倍谎柄。

申請(qǐng)新空間

@SuppressWarnings({"rawtypes","unchecked"})
            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;

這一步?jīng)]什么,直接new了一個(gè)新的數(shù)組

原有數(shù)據(jù)遷移

接下來我們要將原來數(shù)組中的數(shù)據(jù)遷移到新的數(shù)組中來

       for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                  //省略代碼
                }
            }

可以看到球切,這里就是遍歷舊的數(shù)組中的每一個(gè)數(shù)據(jù)谷誓,然后放到新數(shù)組的合適位置上。

  • 遷移沒有產(chǎn)生任何碰撞的數(shù)據(jù)
 if (e.next == null)
        newTab[e.hash & (newCap - 1)] = e;

這里直接用新的容量重新計(jì)算了該元素在新數(shù)組中的位置

  • 碰撞太多已經(jīng)轉(zhuǎn)變成紅黑樹的元素
 else if (e instanceof TreeNode)
         ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

這里對(duì)于紅黑樹的部分比較負(fù)責(zé)吨凑,本文就不做講解,有興趣的可以自己研究

  • 碰撞后產(chǎn)生鏈表的元素
    在看源碼之前户辱,我們先考慮一個(gè)問題:擴(kuò)容后所有元素的位置都需要重新計(jì)算嗎鸵钝?我們舉個(gè)例子說明:
    原有容量為16,有兩個(gè)元素hash值分別為 e1: 2, e2:18,
    2 &(16-1) = 2庐镐;
    18 &(16-1) = 2恩商;
    這兩個(gè)元素都放置在位置2上,現(xiàn)在擴(kuò)容一倍必逆,兩個(gè)元素如果重新計(jì)算位置的話
    2 & (32 -1) = 2;
    18 & (32 -1) = 18;
    可以看到e1的位置還是不變的怠堪,e2的位置雖然變了揽乱,但只是在原有位置上增加了16而已,有了這個(gè)規(guī)律粟矿,我們就可以將鏈表上的元素分為兩種凰棉,一種在新數(shù)組位置不變newTab[j]上,一種在新數(shù)組 newTab[j+oldCap]上陌粹。接下來我們?cè)倏丛创a:
else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }

理解上面說的再看源碼就很好理解了撒犀,就是將原本一條鏈表拆成了兩條,一條放置在newTab[j]上掏秩,一條放置在newTab[j+oldCap]上或舞。至于如何判斷一個(gè)元素是否是在原位置還是在新位置上,這里用了一個(gè)位運(yùn)算:

if ((e.hash & oldCap) == 0) {

如果等于0則在原位置蒙幻,否則就要放置在新位置映凳。

總結(jié)

到這里,hashmap基本就分析的差不多了邮破,剩下的get喷鸽,remove熔恢,update之類的操作也就沒有必要再多說了,有了上面的基礎(chǔ)一看就明白了,希望這篇文章對(duì)大家有幫助雹有,如果有理解不到位的,也請(qǐng)各位指正徘层。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末呵俏,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子范嘱,更是在濱河造成了極大的恐慌送膳,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,039評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件丑蛤,死亡現(xiàn)場(chǎng)離奇詭異叠聋,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)受裹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門碌补,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人棉饶,你說我怎么就攤上這事厦章。” “怎么了照藻?”我有些...
    開封第一講書人閱讀 165,417評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵袜啃,是天一觀的道長。 經(jīng)常有香客問我幸缕,道長群发,這世上最難降的妖魔是什么晰韵? 我笑而不...
    開封第一講書人閱讀 58,868評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮熟妓,結(jié)果婚禮上雪猪,老公的妹妹穿的比我還像新娘。我一直安慰自己滑蚯,他們只是感情好浪蹂,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著告材,像睡著了一般坤次。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上斥赋,一...
    開封第一講書人閱讀 51,692評(píng)論 1 305
  • 那天缰猴,我揣著相機(jī)與錄音,去河邊找鬼疤剑。 笑死滑绒,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的隘膘。 我是一名探鬼主播疑故,決...
    沈念sama閱讀 40,416評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼弯菊!你這毒婦竟也來了纵势?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,326評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤管钳,失蹤者是張志新(化名)和其女友劉穎钦铁,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體才漆,經(jīng)...
    沈念sama閱讀 45,782評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡牛曹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了醇滥。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片黎比。...
    茶點(diǎn)故事閱讀 40,102評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖腺办,靈堂內(nèi)的尸體忽然破棺而出焰手,到底是詐尸還是另有隱情,我是刑警寧澤怀喉,帶...
    沈念sama閱讀 35,790評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站船响,受9級(jí)特大地震影響躬拢,放射性物質(zhì)發(fā)生泄漏躲履。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 331
  • 文/蒙蒙 一聊闯、第九天 我趴在偏房一處隱蔽的房頂上張望工猜。 院中可真熱鬧,春花似錦菱蔬、人聲如沸篷帅。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽魏身。三九已至,卻和暖如春蚪腐,著一層夾襖步出監(jiān)牢的瞬間箭昵,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評(píng)論 1 272
  • 我被黑心中介騙來泰國打工回季, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留家制,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,332評(píng)論 3 373
  • 正文 我出身青樓泡一,卻偏偏與公主長得像颤殴,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子鼻忠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 移步數(shù)據(jù)結(jié)構(gòu)--容器匯總(java & Android)由于 HashMap 底層涉及到太多方面涵但,一篇文章總是不能...
    凱玲之戀閱讀 831評(píng)論 0 5
  • 一、HashMap概述 HashMap基于哈希表的Map接口的實(shí)現(xiàn)粥烁。此實(shí)現(xiàn)提供所有可選的映射操作贤笆,并允許使用nul...
    小陳阿飛閱讀 637評(píng)論 0 2
  • 0、前言 HashMap是Java中最常用的集合之一讨阻,相信大家都使用過芥永,可是你對(duì)他了解嗎?HashMap存儲(chǔ)的數(shù)據(jù)...
    developer_chang閱讀 456評(píng)論 0 4
  • HashMap HashMap概述 HashMap基于哈希表的 Map 接口的實(shí)現(xiàn)钝吮。此實(shí)現(xiàn)提供所有可選的映射操作埋涧,...
    史路比閱讀 294評(píng)論 0 6
  • 轟轟轟! 聶離感覺靈魂海像是炸開了一般奇瘦,里面翻江倒海棘催。 不過隨著時(shí)間的推移,靈魂海慢慢地在擠壓中擴(kuò)大耳标,足足擴(kuò)張了好...
    im喵小姐閱讀 1,206評(píng)論 0 1