HashMap源碼分析

前言


HashMap在開發(fā)中使用非常廣泛盖腕,也是Java面試中必不可少的部分勺鸦。所以適當(dāng)?shù)牧私釮ashMap原理對我們很有幫助并巍。

首先分析下面HashMap的結(jié)構(gòu)圖:

HashMap結(jié)構(gòu)

從上圖我們可以看到:HashMap是由數(shù)組和單向鏈表組成的,左邊是一個Entry<K,V>[] table數(shù)組换途。右邊是一個單向鏈表懊渡,每一個節(jié)點是Entry<K,V>。對于不了解單向鏈表的可以參考我上篇文《Java實現(xiàn)單向鏈表功能》军拟。下面我們開始分析HashMap的源碼剃执,后續(xù)對比一下JDK1.8和JDK1.7的區(qū)別。

問題

在分析源碼之前懈息,我們先提出這幾個問題:

  1. 數(shù)組怎么跟鏈表結(jié)合的肾档?
  2. 哈希沖突是什么?
  3. HashMap的擴容是什么辫继?
  4. 門限值怒见,負載因子,容量和元素數(shù)量究竟有什么關(guān)系姑宽?
  5. 負載因子為什么是0.75遣耍,能否修改?

下面我們先從JDK1.7開始分析源碼炮车,并解決上面的幾個問題舵变。

源碼分析(JDK1.7)

首先看看HashMap幾個關(guān)鍵的成員變量:
    //默認HashMap容量16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    //默認負載因子0.75
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    static final Entry<?,?>[] EMPTY_TABLE = {};
    //首先創(chuàng)建一個table數(shù)組
    transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;

     //門限值
    int threshold;

    //負載因子,默認是0.75
    final float loadFactor;

構(gòu)造函數(shù)

    public HashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

我們平常使用無參的構(gòu)造方法示血,那么默認負載因子就是0.75棋傍,默認容量就是16

put方法

public V put(K key, V value) {
        //如果table數(shù)組為空,那么初始化數(shù)組难审。
        if (table == EMPTY_TABLE) {
            inflateTable(threshold);
        }
        if (key == null)
            return putForNullKey(value);
        //hashcode再次處理
        int hash = hash(key);
        //與運算瘫拣,判斷當(dāng)前插入的元素應(yīng)該放在哪一個table[i]中
        //有可以說成放在第幾個桶中(bucket)
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            //for循環(huán)遍歷當(dāng)前桶的鏈表,如果存在相同的key告喊,則替換當(dāng)前值
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        //如果HashMap不存在該元素麸拄,則新增一個
        addEntry(hash, key, value, i);
        return null;
    }
  1. 先看for循環(huán)部分的代碼:
Entry<K,V> e = table[i]; e != null; e = e.next

結(jié)合注釋我們明白到,要輪詢單向鏈表黔姜,必需從鏈表的頭部開始拢切,那么得出 table[i] 就是我們第i個鏈表的頭部head。

  1. 第1點結(jié)論我們還可以通過addEntry方法驗證秆吵,看代碼:
   void addEntry(int hash, K key, V value, int bucketIndex) {
        //擴容
        if ((size >= threshold) && (null != table[bucketIndex])) {
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
        //創(chuàng)建一個新元素
        createEntry(hash, key, value, bucketIndex);
    }

    void createEntry(int hash, K key, V value, int bucketIndex) {
        Entry<K,V> e = table[bucketIndex];
        //將key淮椰,value傳入創(chuàng)建一個新的Entry,并賦值table[i],
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        //HashMap包含的元素加1
        size++;
    }
static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;
         //創(chuàng)建一個新Entry,并把新創(chuàng)見的next指向傳入的 Entry<K,V> n
        Entry(int h, K k, V v, Entry<K,V> n) {
            value = v;
            next = n;
            key = k;
            hash = h;
        }
}

addEntry擴容部分先不用看主穗,主要看createEntry方法泻拦。
實際上createEntry的工作就是:

  1. 把新put進來的entry賦值給table[i]。
  2. 把當(dāng)前的table[i].next指向以前的table[i]忽媒。跟上篇文章《Java實現(xiàn)單向鏈表功能》add方法類似争拐。

那么我們就可以解決提出的問題:

1.數(shù)組怎么跟鏈表結(jié)合的?
數(shù)組table[i]存儲我們每一條鏈表的頭部head晦雨,而head連接后面新put進來的Entry架曹。
2.哈希沖突是什么?
h哈希沖突簡單來說執(zhí)行put方法之后算出的 i 是相等的闹瞧,相當(dāng)于放在同個table[i],也就是同一條鏈表上绑雄。

擴容resize方法

在addEntry方法我們曾經(jīng)見過resize方法,下面看一下resize邏輯

void resize(int newCapacity) {
     ------------省略部分代碼---------

        //創(chuàng)建一個新的table數(shù)組, table的長度為newCapacity
        Entry[] newTable = new Entry[newCapacity];
        //newTable 賦值操作
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        //把操作完成的的newTable 賦值給table
        table = newTable;
        //門限值賦值等于當(dāng)前的容量乘以負載因子夹抗。
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

    /**
     * Transfers all entries from current table to newTable.
     */
    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        //for,while循環(huán)绳慎,遍歷所有HashMap所有的元素
        for (Entry<K,V> e : table) {
            while(null != e) {
                Entry<K,V> next = e.next;
                //是否重新hash
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                //取出所有的并且計算i,用來判斷放在哪一條鏈表上
                int i = indexFor(e.hash, newCapacity);
                //把屬于該鏈表上的Entry重新串起來
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            }
        }
    }

結(jié)合上面的代碼以及注釋得出擴容過程:

  1. 創(chuàng)建一個新數(shù)組newTable漠烧,長度為newCapacity杏愤。不知道大家還有沒有印象,在addEntry方法中的擴容邏輯 resize(2 * table.length)已脓,那么說明HashMap每一次擴容都是以前的2倍珊楼。
  2. 取出HashMap所有的Entry,重新計算 i 值度液,并且將第 i 條鏈表上的所有Entry重新串起來厕宗。賦值給新創(chuàng)建的newTable 。
  3. 把操作完成的newTable 重新賦值table上堕担。
  4. 重新計算門限值已慢。

那么我們就可以解決提出的問題:

1.HashMap的擴容是什么?
擴容:增加HashMap容量霹购,相當(dāng)于增加table數(shù)組的length佑惠,容納更多新put的Entry。
2.門限值齐疙,負載因子膜楷,容量和元素數(shù)量究竟有什么關(guān)系?
1)門限值 = 負載因子*容量贞奋。
2)那么與元素數(shù)量有什么關(guān)系呢赌厅?在addEntry方法曾經(jīng)看到
if ((size >= threshold) && (null != table[bucketIndex])) {
    resize(2 * table.length);
    --------省略-----------
}
那么也就是說當(dāng)HashMap的size超過門限值的時候,HashMap就會擴容轿塔,一次擴容2倍特愿。

JDK1.7大概就分析到這里仲墨。

JDK1.8的主要變化

JDK1.8比JDK1.7改動了很多,特別性能上優(yōu)化洽议,但HashMap主體結(jié)構(gòu)還是一樣的宗收。下面主要對比一下put方法的邏輯。截取關(guān)鍵的部分代碼分析亚兄。

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

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
       -----------------------省略---------------------------
        //首先計算屬于i值,判斷屬于那一條鏈表尊浓,
        //當(dāng)該條鏈表為空時直接賦值當(dāng)前的Entry县耽。
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
           -----------------------省略---------------------------
               //把新put進來的元素添加鏈表上强重,相當(dāng)于我們JDK1.7的createEntry方法。
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //當(dāng)一條鏈表的Entry的數(shù)量大于TREEIFY_THRESHOLD - 1進行樹化
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }           
                    p = e;
                }
            }
           -----------------------省略---------------------------
        }
        ++modCount;
        //當(dāng)size大于門限值執(zhí)行resize擴容膳叨。
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

從代碼可以看出JDK1.8和JDK1.7邏輯大體相同,值得注意一下的是“樹化”痘系,請看下圖:

樹化
樹化簡單來說就是將以前的鏈表形式改成紅黑樹結(jié)構(gòu)菲嘴,那我們?yōu)槭裁匆某蛇@種結(jié)構(gòu)?
在元素put過程中汰翠,如果發(fā)成哈希沖突龄坪,則算出來的 i 會相等,存放在一個桶中組成一個鏈表复唤。我們都知道鏈表查詢線性的健田,再put過程中都會遍歷某個桶整個鏈表,會嚴重影響存儲性能佛纫。有關(guān)紅黑樹的就不展開了妓局,有興趣的可以查閱相關(guān)資料。

我們剩下最后一個問題

負載因子為什么是0.75呈宇,能否修改好爬?
1. 輪詢長鏈表是耗費性能。HashMap理想狀態(tài)是每一個桶的元素都是相近的甥啄,也就是 0 - i 的鏈表長度都是相近的存炮,性能是最好的。但是現(xiàn)實中并不是型豁,極端時候會遇到某個桶的鏈表有N個數(shù)據(jù)僵蛛,另外一個桶則沒有數(shù)據(jù),因為存數(shù)據(jù)是隨機的迎变,那么就不能保證 i 值能均勻分布充尉。
2.負載因子默認0.75是sun公司經(jīng)過無數(shù)測試總結(jié)出來的,也就是說等于0.75的時候發(fā)生哈希碰撞衣形,性能最好的驼侠。那0.75能否修改姿鸿?可以的。但是不推薦修改倒源。
3.有些人會有疑問苛预,例如我已經(jīng)開辟了16個桶,為什么不存16個元素再重新resize(負載因子等于1)笋熬,這不是浪費空間嗎热某?這個問題答案是:因為哈希碰撞都是隨機的,就算放置第15,16個元素胳螟,元素也不一定分在了15,16桶上昔馋,那么其他桶的鏈表就會變長了,影響性能糖耸。那我16個桶只放8個元素就擴容了(負載因子等于0.5)秘遏?這樣的話就會很浪費內(nèi)存空間了。0.75其實就是一個折中的數(shù)值吧嘉竟。
關(guān)于HashMap的源碼分析到這里為止邦危。下一節(jié)手動實現(xiàn)一個HashMap,歡迎關(guān)注舍扰!
由于作者水平有限倦蚪,如果錯漏歡迎各位大神指出。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末妥粟,一起剝皮案震驚了整個濱河市审丘,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌勾给,老刑警劉巖滩报,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異播急,居然都是意外死亡脓钾,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進店門桩警,熙熙樓的掌柜王于貴愁眉苦臉地迎上來可训,“玉大人,你說我怎么就攤上這事捶枢∥战兀” “怎么了?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵烂叔,是天一觀的道長谨胞。 經(jīng)常有香客問我,道長蒜鸡,這世上最難降的妖魔是什么胯努? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任牢裳,我火速辦了婚禮,結(jié)果婚禮上叶沛,老公的妹妹穿的比我還像新娘蒲讯。我一直安慰自己,他們只是感情好灰署,可當(dāng)我...
    茶點故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布判帮。 她就那樣靜靜地躺著,像睡著了一般溉箕。 火紅的嫁衣襯著肌膚如雪脊另。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天约巷,我揣著相機與錄音,去河邊找鬼旱捧。 笑死独郎,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的枚赡。 我是一名探鬼主播氓癌,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼贫橙!你這毒婦竟也來了贪婉?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤卢肃,失蹤者是張志新(化名)和其女友劉穎疲迂,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體莫湘,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡尤蒿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了幅垮。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片腰池。...
    茶點故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖忙芒,靈堂內(nèi)的尸體忽然破棺而出示弓,到底是詐尸還是另有隱情,我是刑警寧澤呵萨,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布奏属,位于F島的核電站,受9級特大地震影響甘桑,放射性物質(zhì)發(fā)生泄漏拍皮。R本人自食惡果不足惜歹叮,卻給世界環(huán)境...
    茶點故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望铆帽。 院中可真熱鬧咆耿,春花似錦、人聲如沸爹橱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽愧驱。三九已至慰技,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間组砚,已是汗流浹背吻商。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留糟红,地道東北人艾帐。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像盆偿,于是被迫代替她去往敵國和親柒爸。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,060評論 2 355

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

  • HashMap 是 Java 面試必考的知識點事扭,面試官從這個小知識點就可以了解我們對 Java 基礎(chǔ)的掌握程度捎稚。網(wǎng)...
    野狗子嗷嗷嗷閱讀 6,667評論 9 107
  • 一、HashMap概述 HashMap基于哈希表的Map接口的實現(xiàn)求橄。此實現(xiàn)提供所有可選的映射操作今野,并允許使用nul...
    小陳阿飛閱讀 639評論 0 2
  • HashMap HashMap概述 HashMap基于哈希表的 Map 接口的實現(xiàn)。此實現(xiàn)提供所有可選的映射操作谈撒,...
    史路比閱讀 294評論 0 6
  • public class HashMap extends AbstractMap implements Map, ...
    阿桃_28e7閱讀 302評論 0 0
  • Centos7 卸載老版本 安裝新版本 安裝docker-compose "/var/run/docker.soc...
    大風(fēng)0102閱讀 326評論 0 0