HashMap 源碼分析

HashMap對于每一個開發(fā)者來說再熟悉不過了,本文就來講講它究竟是如何實(shí)現(xiàn)的。最開始我并不打算直接對著源碼來說這些疚察,從生活中具體事情來說原理,可以讓人印象更深刻仇奶,也更容易理解貌嫡!

生活小例子

發(fā)現(xiàn)問題

生活中你是一個熱愛看書的人,所以私底下藏有很多書籍猜嘱,像《白夜行》衅枫、《三體》等嫁艇,剛開始你都是把書隨意放在桌子上朗伶,每天看看書喝喝茶,日子過的非常愜意步咪。隨著時(shí)間增長论皆,你的書也越來越多。慢慢的問題出現(xiàn)了猾漫,由于書都是無規(guī)則的放在桌子上点晴,少的時(shí)候還好,這書多了以后悯周,想快速找到需要的那本書越來越困難粒督,可能翻個幾分鐘都找不到,有木有禽翼?

解決問題

這樣下去不是辦法蚣抗,機(jī)智的你靈光一閃毛嫉,想到一個辦法。這樣無規(guī)則的擺放增加了我找書的難度,為了快速找到我需要的書籍织咧,我可以把桌子分為N個區(qū)域,每個區(qū)域用A机隙、B智玻、C這樣的字母來標(biāo)記,然后把書名第一個字的首字母按這個區(qū)域標(biāo)記來規(guī)則的擺放(如:安徒生童話=》A夺脾,白夜行=》B)之拨,當(dāng)我要找書的時(shí)候,可以直接根據(jù)名字就能快速的定位到書在哪個區(qū)域咧叭,找到區(qū)域后再找到想要的那本書蚀乔,速度大大提升了,你心里美滋滋的佳簸,效果圖如下

圖一

書本《安徒生童話》的首字母為A乙墙,因此這本書被你放到了A標(biāo)記的區(qū)域颖变,而《白夜行》和《白鹿原》的首字母都是B,所以它們兩個都應(yīng)該放到B區(qū)域中听想,就這樣你把所有的書籍都按這個規(guī)則整理好了腥刹。相比之前,現(xiàn)在想找一本書簡直快的多了汉买,比如我想要看《三體》衔峰,直接找到書桌上S標(biāo)記的區(qū)域,然后在這區(qū)域疊起來的一摞書中找就是了蛙粘!這個時(shí)候你太佩服自己了垫卤。

問題升級

這樣的設(shè)計(jì)讓你舒舒服服的過了很久,可是時(shí)間久了你又遇到一個問題了:B區(qū)域的書太多了出牧。我找一本首字母為B開頭的書還是需要費(fèi)很大力氣呀穴肘,這個時(shí)候你又陷入了沉思,能不能對這里稍微再改進(jìn)呢舔痕?

最終方案

有一天评抚,你在X寶無意中發(fā)現(xiàn)一個東西

圖二(圖片來源網(wǎng)絡(luò))

咦,這個東西不錯呀伯复,把某個區(qū)域過多的書用這個代替慨代,在這個書架中你可以再設(shè)定自己的一套規(guī)則(比如書名不超過5個字的可以放右側(cè),超過了的就放左側(cè))啸如,方便更快速的找書侍匙,完美!當(dāng)然你不會每個區(qū)域都買個小書架叮雳,這樣成本太高了想暗,所以你決定每當(dāng)某個區(qū)域的書本多于N的時(shí)候(N由你自己決定),你就放一個小書架到這個區(qū)域里面债鸡,最終方案如下

圖三

經(jīng)過上面的文字江滨,你知道HashMap的原理了嗎?

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

jdk1.7結(jié)構(gòu)圖

如果仔細(xì)讀了上面的小故事厌均,我想HashMap的原理大概已經(jīng)知道了唬滑,現(xiàn)在我們來看下HashMap1.7的結(jié)構(gòu)圖

圖四

看到這個圖是不是和上文中的書桌與書本極其相識?這里的table[]數(shù)組表示書桌棺弊,數(shù)組的每一個元素代表書桌的標(biāo)記字母的區(qū)域晶密,而entry節(jié)點(diǎn)表示書本。HashMap不斷的存取數(shù)據(jù)(put方法)其實(shí)就是不斷的向書桌增加書本模她。在HashMap里面有個概念叫Hash沖突稻艰,就是類似不同書本的首字母可能會相同,在書桌上我們采用一本本的疊加起來解決問題侈净,相當(dāng)于上圖當(dāng)中的鏈表尊勿。

jdk1.8結(jié)構(gòu)圖

在上面的故事中已經(jīng)提到了僧凤,當(dāng)書桌某個區(qū)域中的書越來越多的時(shí)候,我們在里面加了個小書架元扔,因此在HashMap1.8中同樣為了優(yōu)化區(qū)域中搜索而引入了紅黑樹躯保,來看下HashMap1.8的結(jié)構(gòu)圖

圖五

在HashMap1.8中引入了紅黑樹,能在鏈表長度過多的時(shí)候澎语,增加查詢速度途事。這就跟上面書桌區(qū)域中引入一個書架是一個道理,由于書太多擅羞,一本本的查詢還是會花比較多的時(shí)間尸变,針對區(qū)域進(jìn)行優(yōu)化,可以更快速的找到想要的書减俏。

好了召烂,說了這么多,是時(shí)候進(jìn)入主題了垄懂。

HashMap 源碼解析

關(guān)鍵變量

在HashMap源碼中有幾個關(guān)鍵變量骑晶,我們必須知道,通過這些變量我們可以更容易理解它的底層原理草慧,注意這里的源碼是基于1.8版本的

    /**
     * The default initial capacity - MUST be a power of two.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * The bin count threshold for using a tree rather than list for a
     * bin.  Bins are converted to trees when adding an element to a
     * bin with at least this many nodes. The value must be greater
     * than 2 and should be at least 8 to mesh with assumptions in
     * tree removal about conversion back to plain bins upon
     * shrinkage.
     */
    static final int TREEIFY_THRESHOLD = 8;

    /**
     * The bin count threshold for untreeifying a (split) bin during a
     * resize operation. Should be less than TREEIFY_THRESHOLD, and at
     * most 6 to mesh with shrinkage detection under removal.
     */
    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;
  • static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;默認(rèn)起始容量匙头,就是table數(shù)組的默認(rèn)長度(2的4次方)漫谷,相當(dāng)于書桌上的區(qū)域數(shù)量,必須為2的N次方(為什么蹂析?后面解釋)舔示。

  • static final int MAXIMUM_CAPACITY = 1 << 30;最大容量电抚,table數(shù)組的長度不能超過這個(2的30次方)惕稻,即使超過也不會再改變。

  • static final float DEFAULT_LOAD_FACTOR = 0.75f蝙叛;加載因子俺祠,這個干嘛的呢?我們知道table是一個數(shù)組借帘,初始化的時(shí)候都必須指定一個長度蜘渣,隨著HashMap不斷的put東西進(jìn)去,這個數(shù)組需要擴(kuò)容也就是增加長度肺然。這個時(shí)候就是根據(jù)這個加載因子來擴(kuò)容的蔫缸,初始化的時(shí)候?yàn)?6,當(dāng)數(shù)組中的元素?cái)?shù)量達(dá)到16*0.75=12的時(shí)候际起,table長度會變成原來的兩倍拾碌。

  • static final int TREEIFY_THRESHOLD = 8吐葱;鏈表轉(zhuǎn)紅黑樹閾值,當(dāng)鏈表數(shù)量超過這個數(shù)的時(shí)候校翔,它會將鏈表轉(zhuǎn)為紅黑樹(這里并不打算講紅黑樹的特性唇撬,具體百度,嚴(yán)格來說還有一個因素會影響是否轉(zhuǎn)化成紅黑樹MIN_TREEIFY_CAPACITY展融,具體往下看)窖认。

  • static final int UNTREEIFY_THRESHOLD = 6;紅黑樹轉(zhuǎn)鏈表閾值告希,當(dāng)紅黑樹數(shù)量小于這個數(shù)的時(shí)候扑浸,它會將紅黑樹轉(zhuǎn)變?yōu)殒湵怼?/p>

  • static final int MIN_TREEIFY_CAPACITY = 64;這個也是鏈表轉(zhuǎn)為紅黑樹的一個條件燕偶,前面提到的一個條件是鏈表中的個數(shù)要大于8喝噪。而這里則是table數(shù)組的長度需要超過64,也就是說只有兩個條件達(dá)到才會由鏈表轉(zhuǎn)化為紅黑樹指么。

關(guān)鍵類與方法

hash()方法

hash方法對應(yīng)將書本名稱換算成字母的一個方法酝惧,如hash("安徒生童話") = A,貼上源碼

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

該方法能將一個key轉(zhuǎn)換成int型的值伯诬。需要注意的是不同的key經(jīng)過hash方法得到的值有可能是相同的晚唇,這就是所謂的hash沖突,前面也提到了盗似,解決hash沖突主要采用的是拉鏈?zhǔn)降逆湵斫Y(jié)構(gòu)哩陕。從代碼上我們也可以知道,當(dāng)key為null的時(shí)候赫舒,統(tǒng)一規(guī)定hash返回為0悍及,也就是說key為null的鍵值對會被放在table[0]這個區(qū)域里面(類似書桌第一塊區(qū)域)。

Node類

Node是HashMap中的一個內(nèi)部類接癌,充當(dāng)書桌上的書這一角色心赶,組成鏈表的關(guān)鍵,先來看源碼

    /**
     * Basic hash bin node, used for most entries.  (See below for
     * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
     */
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

用通俗的話來說明上面的具體參數(shù)缺猛,hash指書名經(jīng)過規(guī)則換算成A字母缨叫,key值書本名稱,value指具體的書枯夜,next蓋在當(dāng)前書上書籍弯汰,形成拉鏈?zhǔn)降慕Y(jié)構(gòu)。

put(K key, V value)方法

看了源碼的話我們會發(fā)現(xiàn)湖雹,在HashMap中的put方法實(shí)際上是調(diào)用了另外一個方法putVal咏闪。

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

因此我們接下來看putVal這個方法,每行都補(bǔ)充了相關(guān)注釋

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //將table賦值給tab摔吏,如果table為空或者長度為0鸽嫂,則調(diào)用resize()初始化纵装,再把長度賦值給n
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //這里(n - 1) & hash相當(dāng)于hash%(n-1),判斷tab[i]這個區(qū)域是否已經(jīng)有值据某,如果沒有橡娄,則新建一個節(jié)點(diǎn),并且賦值給p癣籽,把節(jié)點(diǎn)放到這個區(qū)域
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        //如果tab[i]里面已經(jīng)存在值了
        else {
            Node<K,V> e; K k;
            //如果p這個節(jié)點(diǎn)的傳進(jìn)來的是一樣的挽唉,則把p賦值給e
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            //如果p為樹類型節(jié)點(diǎn),則調(diào)用樹的putTreeVal方法筷狼,此方法就是一個紅黑樹添加
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //遍歷區(qū)域中所有節(jié)點(diǎn)
            else {
                for (int binCount = 0; ; ++binCount) {
                    //將p的子節(jié)點(diǎn)賦值給e瓶籽,如果區(qū)域中沒有相同key的節(jié)點(diǎn)則直接插入到區(qū)域中(可能是鏈表可能是紅黑樹,具體查看treeifyBin方法)埂材,退出循環(huán)
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //如果區(qū)域中有相同key的節(jié)點(diǎn)直接退出循環(huán)塑顺,
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    //以上都沒有,則e賦值給p繼續(xù)循環(huán)
                    p = e;
                }
            }
            //e不為空俏险,說明之前區(qū)域中存在key與現(xiàn)在新的key相同严拒,這時(shí)候?qū)⑿轮蹈采w原來的舊值,并且返回舊值
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

另外再附上一張put方法完整流程圖(點(diǎn)擊可看大圖)

圖6(來源網(wǎng)絡(luò))

get(Object key)方法

同樣竖独,從源碼中可以看到主要是調(diào)用了getNode這個方法

    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

下面貼上getNode的源碼裤唠,里面也添加了相關(guān)注釋

    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        //判斷table是否為空并且長度不為0,根據(jù)hash得到應(yīng)該存放的區(qū)域预鬓,區(qū)域頭節(jié)點(diǎn)也不為空
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            //是否與頭結(jié)點(diǎn)的key相同巧骚,相同則直接返回該節(jié)點(diǎn)
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            //頭結(jié)點(diǎn)不匹配則進(jìn)入遍歷
            if ((e = first.next) != null) {
                //如果是樹節(jié)點(diǎn),遍歷紅黑樹格二,找到節(jié)點(diǎn)立即返回該節(jié)點(diǎn),具體紅黑樹的遍歷查詢需要查看getTreeNode方法
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                //否則遍歷鏈表竣蹦,找到節(jié)點(diǎn)立即返回該節(jié)點(diǎn)
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        //沒有找到對應(yīng)的key顶猜,說明不存在,返回null;
        return null;
    }

總結(jié)

源碼分析并不難痘括,需要一顆平靜的心长窄,切勿浮躁。每個人的理解都不一樣纲菌,選擇自己的方式挠日,效果會更明顯。另外就是不能被一些專業(yè)的術(shù)語所嚇到翰舌,很多所謂的術(shù)語其實(shí)很簡單嚣潜。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市椅贱,隨后出現(xiàn)的幾起案子懂算,更是在濱河造成了極大的恐慌只冻,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,273評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件计技,死亡現(xiàn)場離奇詭異喜德,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)垮媒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,349評論 3 398
  • 文/潘曉璐 我一進(jìn)店門舍悯,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人睡雇,你說我怎么就攤上這事萌衬。” “怎么了入桂?”我有些...
    開封第一講書人閱讀 167,709評論 0 360
  • 文/不壞的土叔 我叫張陵奄薇,是天一觀的道長。 經(jīng)常有香客問我抗愁,道長馁蒂,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,520評論 1 296
  • 正文 為了忘掉前任蜘腌,我火速辦了婚禮沫屡,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘撮珠。我一直安慰自己沮脖,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,515評論 6 397
  • 文/花漫 我一把揭開白布芯急。 她就那樣靜靜地躺著勺届,像睡著了一般。 火紅的嫁衣襯著肌膚如雪娶耍。 梳的紋絲不亂的頭發(fā)上免姿,一...
    開封第一講書人閱讀 52,158評論 1 308
  • 那天,我揣著相機(jī)與錄音榕酒,去河邊找鬼胚膊。 笑死,一個胖子當(dāng)著我的面吹牛想鹰,可吹牛的內(nèi)容都是我干的紊婉。 我是一名探鬼主播,決...
    沈念sama閱讀 40,755評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼辑舷,長吁一口氣:“原來是場噩夢啊……” “哼喻犁!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,660評論 0 276
  • 序言:老撾萬榮一對情侶失蹤株汉,失蹤者是張志新(化名)和其女友劉穎筐乳,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體乔妈,經(jīng)...
    沈念sama閱讀 46,203評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蝙云,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,287評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了路召。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片勃刨。...
    茶點(diǎn)故事閱讀 40,427評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖股淡,靈堂內(nèi)的尸體忽然破棺而出身隐,到底是詐尸還是另有隱情,我是刑警寧澤唯灵,帶...
    沈念sama閱讀 36,122評論 5 349
  • 正文 年R本政府宣布贾铝,位于F島的核電站,受9級特大地震影響埠帕,放射性物質(zhì)發(fā)生泄漏垢揩。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,801評論 3 333
  • 文/蒙蒙 一敛瓷、第九天 我趴在偏房一處隱蔽的房頂上張望叁巨。 院中可真熱鬧,春花似錦呐籽、人聲如沸锋勺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,272評論 0 23
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽庶橱。三九已至,卻和暖如春贪惹,著一層夾襖步出監(jiān)牢的瞬間悬包,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,393評論 1 272
  • 我被黑心中介騙來泰國打工馍乙, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人垫释。 一個月前我還...
    沈念sama閱讀 48,808評論 3 376
  • 正文 我出身青樓丝格,卻偏偏與公主長得像,于是被迫代替她去往敵國和親棵譬。 傳聞我的和親對象是個殘疾皇子显蝌,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,440評論 2 359

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

  • HashMap 是 Java 面試必考的知識點(diǎn),面試官從這個小知識點(diǎn)就可以了解我們對 Java 基礎(chǔ)的掌握程度。網(wǎng)...
    野狗子嗷嗷嗷閱讀 6,669評論 9 107
  • JAVA 8 HashMap 源碼分析 一 什么是HashMap曼尊? HashMap 繼承了AbstractMap,...
    gdutkyle閱讀 466評論 0 1
  • HashMap源碼分析——JDK1.8 轉(zhuǎn)載:http://blog.csdn.net/u010498696/ar...
    SinX竟然被占用了閱讀 284評論 0 0
  • 《古希臘羅馬神話故事》是一本很有趣的書酬诀,也是一本很有意思的書。最初挑選這本書骆撇,就是因?yàn)闀饔蚁肓私夤畔ED羅馬...
    木槿之淚閱讀 5,513評論 0 4
  • 云對雨,雪對風(fēng)神郊,佛陀對蒼生肴裙。我對你,嘴對心涌乳,九夏對三冬蜻懦。 一個人生命中最大的幸運(yùn),莫過于在他的人生中途夕晓,還年富力強(qiáng)...
    麥琪957閱讀 335評論 0 0