HashMap1.8源碼分析

本文主要有關(guān)HashMap1.8源碼分析

原理和過程

1:HashMap的原理厘肮,內(nèi)部數(shù)據(jù)結(jié)構(gòu)如何吉懊?

底層使用哈希表(數(shù)組+鏈表)褐澎,當(dāng)鏈表過長(其實(shí)是大于8)的時(shí)候會(huì)將鏈表轉(zhuǎn)換成紅黑樹疮胖,以實(shí)現(xiàn)n log(n) 的查找俯萎,每一個(gè)節(jié)點(diǎn)Node包括key凉袱,value芥吟,Node,Next指針。
[圖片上傳失敗...(image-eeb83d-1551951086064)]

2:具體過程

  • 1:對 Key 求 Hash 值绑蔫,然后再計(jì)算 下標(biāo)运沦。
  • 2:如果沒有碰撞,直接放入桶中配深,
  • 3:如果碰撞了携添,以鏈表的方式鏈接到后面,
  • 4:如果鏈表長度超過閥值(TREEIFY_THRESHOLD == 8)篓叶,就把鏈表轉(zhuǎn)成紅黑樹烈掠。
  • 5:如果節(jié)點(diǎn)已經(jīng)存在就替換舊值
  • 6:如果桶滿了(容量 * 加載因子)羞秤,就需要 resize。
    • i:在調(diào)用put方法的時(shí)候左敌,底層直接調(diào)用的是putVal方法

源碼分析

    putAll()方法
    putVal(hash(key), key, value, false, true);
    
    hash(key) 方法
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
     }

為什么不上直接返回key.hashCode()---這是由jvm決定的瘾蛋,就是key的地址;而是返回key的hashCode 該值的高16位與其低16位異或后的值呢
首先矫限,h是32位的哺哼,而采用以后運(yùn)算得到的0和1是均勻的,因?yàn)槲覀儜?yīng)該盡量使這個(gè)值不同叼风,& | 運(yùn)算都會(huì)偏向0或1取董,如下圖所示
[圖片上傳失敗...(image-c13d0d-1551951086064)]

putVal(.......)方法源碼分析

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    // 其中Node就是K和V的一個(gè)節(jié)點(diǎn),一個(gè)內(nèi)部類
    Node<K,V>[] tab; 
    //定義一個(gè)p節(jié)點(diǎn)
    Node<K,V> p; int n, i;
    //table是Node數(shù)組无宿,其默認(rèn)length為16 (2的n次冪茵汰,為什么要這樣呢 有一定道理的,后面解釋)
    if ((tab = table) == null || (n = tab.length) == 0)
    //如果tab為空孽鸡,或者長度為0蹂午,說明map中還沒有存放value
        n = (tab = resize()).length; //此時(shí)需要調(diào)用resize()方法擴(kuò)容,這里是初始化彬碱,這個(gè)方法后面分析
    if ((p = tab[i = (n - 1) & hash]) == null)  
    //說明這個(gè)位置為空豆胸,直接生成一個(gè)K和V的Node節(jié)點(diǎn),插入即可
         tab[i] = newNode(hash, key, value, null);
    //為什么 i = (n - 1) & hash呢 而不是直接 hash % n 呢
    //假設(shè)n的值為16巷疼,-1的話為15 : 000....001111 
     //   hash   :  1010....11010   //所以明顯得到的值的范圍為(0-15)配乱,和%運(yùn)算一樣,                            
        //可效率明顯&高哦皮迟,直接轉(zhuǎn)為2進(jìn)制了

    //該位置有Node了,hash碰撞了桑寨,這時(shí)候有三種處理情況
    //(1)key相同 伏尼,直接替換為新的節(jié)點(diǎn)
    //(2)key不同,用鏈表存儲
    //(3) key不同尉尾,用紅黑樹存儲 
     else {   
        Node<K,V> e; K k;
        //key相同爆阶,直接替換
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
            //紅黑數(shù)方式存儲(需要判斷該節(jié)點(diǎn)是在左邊還是右邊,還要左旋和右旋進(jìn)行調(diào)整)
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        //鏈表沙咏,尾插法存儲
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) { //判斷p節(jié)點(diǎn)下一節(jié)點(diǎn)是否為空
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        //鏈表轉(zhuǎn)紅黑樹
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;  //這種情況會(huì)執(zhí)行下面的if(e != null)
                p = e;
            }
        }
        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)  //這個(gè)threshold就是用下判斷是否需要擴(kuò)容的
        resize();
    afterNodeInsertion(evict);
    return null;
}

問題

  • 1:HashMap 中 hash 函數(shù)怎么是是實(shí)現(xiàn)的辨图?

    • 1:高 16bit 不變,低 16bit 和高 16bit 做了一個(gè)異或
    • 2:(n - 1) & hash --> 得到下標(biāo)
  • 2:數(shù)組每個(gè)位置上的鏈表長度為什么要限制長度呢肢藐?
    如果在某個(gè)位置上發(fā)生過多hash碰撞故河,在put的時(shí)候就會(huì)發(fā)生順延,要從頭到尾比遍歷這個(gè)鏈表吆豹,
    效率低 (put的時(shí)候是尾插法)鱼的,所以在jdk1.8的時(shí)候長度超過8的時(shí)候就轉(zhuǎn)為紅黑樹理盆。
    [圖片上傳失敗...(image-dbe133-1551951086064)]

  • 3:怎么減少hash碰撞,盡可能的使數(shù)組位置都用到凑阶?
    i = (n-1) & hash,所以是由n 和 hash控制的猿规,顯然我們能夠操作的是hash,就是通過key的hashCode的高16位和低16異或宙橱。

  • 4:為什么n一定要是n的2次冪呢姨俩?
    其實(shí)就是為了保證在計(jì)算i的時(shí)候,在&操作的時(shí)候师郑,(n -1)獲的每一位都為1环葵,此時(shí)i的值才取決有hash。

  • 5:HashMap 怎樣解決沖突呕乎,講一下擴(kuò)容過程积担,假如一個(gè)值在原數(shù)組中,現(xiàn)在移動(dòng)了新數(shù)組猬仁,位置肯定改變了帝璧,那是什么定位到在這個(gè)值新數(shù)組中的位置(可看下面擴(kuò)容源碼分析解釋)

    • a:將新節(jié)點(diǎn)加到鏈表后,
    • b:容量擴(kuò)充為原來的兩倍湿刽,然后對每個(gè)節(jié)點(diǎn)重新計(jì)算哈希值的烁。
    • c:這個(gè)值只可能在兩個(gè)地方,一個(gè)是原下標(biāo)的位置诈闺,另一種是在下標(biāo)為 <原下標(biāo)+原容量> 的位置渴庆。
  • 6:幾個(gè)參數(shù)意思


    //數(shù)組默認(rèn)的容量:1左移4位  16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    //最大容量 2的30次冪
    static final int MAXIMUM_CAPACITY = 1 << 30;
    //加載因子,用來乘以容量雅镊,算出一個(gè)判斷擴(kuò)容的數(shù)值
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    //鏈表長度超過這個(gè)長度后轉(zhuǎn)為紅黑樹
    static final int TREEIFY_THRESHOLD = 8;
    

擴(kuò)容

擴(kuò)容源碼分析:雙倍擴(kuò)容(保證容量為2的n次冪)

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
 if (oldCap >= MAXIMUM_CAPACITY) {  //不能再擴(kuò)容了 
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
    newThr = oldThr << 1; // 擴(kuò)容后襟雷,這個(gè)閾值也要翻倍
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
         //oldCap = 0 ,這里就是初始化
        newCap = DEFAULT_INITIAL_CAPACITY;
        //這個(gè)數(shù)值是干嘛的呢,就是用來判斷什么時(shí)候開始對這個(gè)數(shù)組進(jìn)行擴(kuò)容仁烹,
        // 容量*加載因子 < 容量
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 12
    }
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];  //擴(kuò)容數(shù)組
    table = newTab;
     //擴(kuò)容之后耸弄,之前有的元素位置必須要調(diào)整,進(jìn)行遷移
    if (oldTab != null) {
        //數(shù)組對應(yīng)位置下面沒元素 是鏈表 是紅黑樹
        //開始循環(huán)遍歷數(shù)組
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e; //得到新的位置
                else if (e instanceof TreeNode) //紅黑樹
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                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) {
                        //e.hash & oldCap 對這個(gè)進(jìn)行分析 加入oldCap = 16
                        //我們之前計(jì)算i的時(shí)候是通過 hash & oldCap -1  (15:01111);
                        //而在擴(kuò)容的時(shí)候 用 hash & oldCap (16:10000)這個(gè)值與0 比較
                        //如果等于0 能說明什么呢 即hash的二級制的倒數(shù)第5位為0
                        //newCap = 32 新的 i = hash & 31 (11111) 會(huì)等于 hash & 15(01111)
                        //即在數(shù)組中的位置沒變
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else { //不是為0 i的值會(huì)等于  之前的位置 + oldCap
                            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; //巧妙把计呈,看while中的循環(huán)解釋
                    }
                }
            }
        }
    }
    return newTab;
}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市征唬,隨后出現(xiàn)的幾起案子捌显,更是在濱河造成了極大的恐慌,老刑警劉巖总寒,帶你破解...
    沈念sama閱讀 218,451評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件扶歪,死亡現(xiàn)場離奇詭異,居然都是意外死亡偿乖,警方通過查閱死者的電腦和手機(jī)击罪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評論 3 394
  • 文/潘曉璐 我一進(jìn)店門哲嘲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人媳禁,你說我怎么就攤上這事眠副。” “怎么了竣稽?”我有些...
    開封第一講書人閱讀 164,782評論 0 354
  • 文/不壞的土叔 我叫張陵囱怕,是天一觀的道長。 經(jīng)常有香客問我毫别,道長娃弓,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,709評論 1 294
  • 正文 為了忘掉前任岛宦,我火速辦了婚禮台丛,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘砾肺。我一直安慰自己挽霉,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,733評論 6 392
  • 文/花漫 我一把揭開白布变汪。 她就那樣靜靜地躺著侠坎,像睡著了一般。 火紅的嫁衣襯著肌膚如雪裙盾。 梳的紋絲不亂的頭發(fā)上实胸,一...
    開封第一講書人閱讀 51,578評論 1 305
  • 那天,我揣著相機(jī)與錄音番官,去河邊找鬼庐完。 笑死,一個(gè)胖子當(dāng)著我的面吹牛徘熔,可吹牛的內(nèi)容都是我干的假褪。 我是一名探鬼主播,決...
    沈念sama閱讀 40,320評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼近顷,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了宁否?” 一聲冷哼從身側(cè)響起窒升,我...
    開封第一講書人閱讀 39,241評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎慕匠,沒想到半個(gè)月后饱须,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,686評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡台谊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,878評論 3 336
  • 正文 我和宋清朗相戀三年蓉媳,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了譬挚。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,992評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡酪呻,死狀恐怖减宣,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情玩荠,我是刑警寧澤漆腌,帶...
    沈念sama閱讀 35,715評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站阶冈,受9級特大地震影響闷尿,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜女坑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,336評論 3 330
  • 文/蒙蒙 一填具、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧匆骗,春花似錦劳景、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至铝噩,卻和暖如春衡蚂,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背骏庸。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評論 1 270
  • 我被黑心中介騙來泰國打工毛甲, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人具被。 一個(gè)月前我還...
    沈念sama閱讀 48,173評論 3 370
  • 正文 我出身青樓玻募,卻偏偏與公主長得像,于是被迫代替她去往敵國和親一姿。 傳聞我的和親對象是個(gè)殘疾皇子七咧,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,947評論 2 355