HashMap分析小結(jié)

HashMap是Java使用頻率很高的容器對(duì)象,內(nèi)部使用了很多優(yōu)化算法,源碼非常值得學(xué)習(xí).

關(guān)于HashMap

  • 非線程安全

HashTable對(duì)put和get使用了synchronized關(guān)鍵字,線程安全,但是已經(jīng)被廢棄,ConcurrentHashMap是推薦使用的線程安全,高并發(fā)Map實(shí)現(xiàn)

  • key-value存儲(chǔ)
  • key和value都可以為null,多個(gè)為null的key會(huì)被后面的覆蓋
  • key要求為不可變對(duì)象(引用類型必須重寫hashCode和equals方法)

為了確保同一個(gè)對(duì)象的hash計(jì)算后的值唯一,不同的對(duì)象hash計(jì)算后的值一定不等.

  • HashMap內(nèi)部存儲(chǔ)結(jié)構(gòu)為數(shù)組+鏈表+紅黑樹(shù)(JDK1.8開(kāi)始)
HashMap存儲(chǔ)結(jié)構(gòu)

HashMap存儲(chǔ)結(jié)構(gòu)

在HashMap內(nèi)部,有一個(gè)Node[] table 字段,Node類型就是數(shù)據(jù)保存在HashMap內(nèi)部時(shí)的實(shí)際對(duì)象,Node實(shí)現(xiàn)Map.Entry接口,本質(zhì)就是一個(gè)鍵值對(duì),Node對(duì)象會(huì)持有下一個(gè)結(jié)點(diǎn)的引用,由此可知Node對(duì)象又維護(hù)了一個(gè)單向鏈表.

//HashMap中Node對(duì)象
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) {...}
       ...
}

HashMap使用哈希表的意義

HashMap使用了哈希表來(lái)存儲(chǔ),當(dāng)key值哈希沖突之后使用鏈表保存沖突的值,當(dāng)數(shù)據(jù)哈希計(jì)算之后得到數(shù)組下標(biāo),把數(shù)據(jù)放在對(duì)應(yīng)下標(biāo)的鏈表上.

Map<String,Integer> map = new HashMap<>();
map.put("key",123);

使用hash算法是為了盡量減少hash沖突,如果默認(rèn)的node數(shù)組很大,那么發(fā)生沖突的幾率也會(huì)減小,但是會(huì)浪費(fèi)很多的內(nèi)存空間,為了平衡效率和空間,HashMap采用了負(fù)載因子(loadFactor)和擴(kuò)容提高空間使用率,提高存取效率.075是對(duì)空間和時(shí)間效率的一個(gè)平衡選擇,不建議自行修改,除非對(duì)內(nèi)存和時(shí)間效率有取舍有要求時(shí)才會(huì)進(jìn)行修改.

負(fù)載因子的作用是控制HashMap擴(kuò)容的時(shí)機(jī),默認(rèn)為0.75,HashMap初始的table大小為16.
簡(jiǎn)單來(lái)說(shuō) : 當(dāng)存儲(chǔ)數(shù)量>table.length*0.75時(shí),就會(huì)觸發(fā)HashMap擴(kuò)容

//HashMap成員變量
//默認(rèn)負(fù)載因子為0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f; 
//默認(rèn)桶的大小,1左移4位,就是16
 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4
HashMap的table變量

根據(jù)上面這段注釋,可以知道HashMap內(nèi)部的數(shù)組只有在第一次使用的時(shí)候才會(huì)被初始化,在必要的時(shí)候會(huì)進(jìn)行擴(kuò)容,而且數(shù)組的長(zhǎng)度總是2的n次方數(shù),使用2的n次方的原因是為了在模運(yùn)算和擴(kuò)容是進(jìn)行優(yōu)化,同時(shí)為了減少?zèng)_突,HashMap定位哈希桶索引位置時(shí),使用了高位運(yùn)算.

HashMap使用了很多的算法和優(yōu)化提高性能,但是當(dāng)數(shù)據(jù)量很大時(shí),哈希沖突無(wú)法避免,使用鏈表會(huì)導(dǎo)致數(shù)據(jù)的查找性能急劇下降,所以在JDK1.8加入了紅黑樹(shù),當(dāng)鏈表長(zhǎng)度達(dá)到8時(shí).鏈表會(huì)轉(zhuǎn)為紅黑樹(shù),利用紅黑樹(shù)快速增刪改查的特點(diǎn)提高HashMap的性能.

由于紅黑樹(shù)是另一個(gè)知識(shí)點(diǎn),不會(huì)在HashMap的小結(jié)中出現(xiàn).

功能實(shí)現(xiàn)-方法

HashMap的內(nèi)部實(shí)現(xiàn)了很多算法和功能,其中三個(gè)最具有代表性的方法是:根據(jù)key計(jì)算哈希桶數(shù)組索引下標(biāo),put方法,擴(kuò)容.所以對(duì)這三個(gè)方法進(jìn)行深入.

1.確定哈希桶數(shù)組索引位置

不管增刪改查,第一步操作都是根據(jù)key的hash值獲取key在哈希桶中的下標(biāo).由于HashMap的數(shù)據(jù)結(jié)構(gòu)是數(shù)組+鏈表,由于數(shù)組的訪問(wèn)速度是最快的,所以應(yīng)該盡量將存入的元素分布在不同的數(shù)組下標(biāo)中,使得每個(gè)位置上的元素只有一個(gè),當(dāng)使用hash算法求得這個(gè)位置的時(shí)候,對(duì)應(yīng)下標(biāo)的元素就是所需元素,不需要遍歷該位置上的鏈表,所以查詢效率會(huì)很高.

數(shù)組在在內(nèi)存中是連續(xù)的,所以查詢效率是最高的,而鏈表是不連續(xù)的內(nèi)存空間,每一次查詢都需要遍歷鏈表.

確定下標(biāo)的步驟:

  • 步驟1
    • 取key的hashCode
    • key的hashCode無(wú)符號(hào)右移16位
    • 右移后的值與右移前的值做與運(yùn)算.
static final int hash(Object key) {
        int h;
    
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
  • 步驟2
    • 將調(diào)用hash(Object key)方法后獲取的值和哈希桶長(zhǎng)度-1做與運(yùn)算
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
...
int n ;
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
...
}

這里就是hash算法確定下標(biāo)的算法,本質(zhì)上的流程為:取key的hashCode值送朱、高位運(yùn)算邪蛔、取模運(yùn)算滞谢。

由于HashMap使用了hashCode,所以只要確保key對(duì)象的不變性,那么調(diào)用hash(Object key)就一定能獲取相同的哈希值,其次因?yàn)橐_保下標(biāo)要在哈希桶內(nèi),所以比較容易想到的是對(duì)哈希值和桶長(zhǎng)度進(jìn)行取模,這樣就能保證元素的分布相對(duì)均勻.但是模運(yùn)算消耗較大,所以在HashMap中的做法是使用h&(table.length-1),根據(jù)之前的分析可以,哈希桶的長(zhǎng)度是2的n次方,所以table.length-1之后,二進(jìn)制后的數(shù)字全部都是1,所以無(wú)論h的值是什么,都相當(dāng)于取模的結(jié)果,但是&比%效率更高.

&比%效率高的證明

例如table.length = 16, h =5;
1111&0101 = 0101 ,即等于5,哈希桶下標(biāo)為5

圖1-1 h^(table.length-1)計(jì)算
2.HashMap的put方法
圖2-2 HashMap的put方法執(zhí)行流程
  • ①判斷HashMap的哈希桶是否為null,通過(guò)resize()方法進(jìn)行擴(kuò)容.
  • ②判斷哈希桶下標(biāo)是否存在元素,不存在則插入元素
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長(zhǎng)度,
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        //②判斷下標(biāo)位置元素是否為空,如果為空插入一個(gè)新的值
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            ...//這里涉及到hash沖突后的處理
      }
}
  • ③當(dāng)前元素不為空,證明發(fā)生了hash沖突,所以判斷兩個(gè)新舊兩個(gè)key的值是否相同或相等,如果相等,用新value覆蓋舊value
  • ④如果新插入的結(jié)點(diǎn)是TreeNode,即判斷table[i]是否為紅黑樹(shù),如果是則在樹(shù)中插入該值,否則繼續(xù)執(zhí)行后序代碼
  • ⑤遍歷table[i],判斷鏈表中結(jié)點(diǎn)是否有后繼結(jié)點(diǎn),如果后繼結(jié)點(diǎn)為空則插入到隊(duì)尾,同時(shí)判斷當(dāng)前鏈表長(zhǎng)度是否大于8,如果大于8,則將鏈表轉(zhuǎn)為紅黑樹(shù)
  • ⑥在遍歷鏈表的過(guò)程中,如果發(fā)現(xiàn)新值的key已存在鏈表中,則覆蓋舊的value為新value
  • ⑦插入成功后,判斷實(shí)際存在的鍵值對(duì)數(shù)量size是否大于負(fù)載容量thredshold,如果超過(guò),就調(diào)用resize()進(jìn)行擴(kuò)容
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
...
 Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                //當(dāng)前鏈表為紅黑樹(shù)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                //遍歷鏈表
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        //將新值插入到鏈表隊(duì)尾,
                        p.next = newNode(hash, key, value, null);
                        //判斷當(dāng)前鏈表長(zhǎng)度是否大于8,是就轉(zhuǎn)為紅黑樹(shù)
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    ⑥覆蓋鏈表中的舊value
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        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;
            }
         ++modCount;
        if (++size > threshold)
         //擴(kuò)容
            resize();
        afterNodeInsertion(evict);
        return null;
}
3.擴(kuò)容機(jī)制

擴(kuò)容就是重新計(jì)算HashMap中哈希桶的大小,向HashMap中不斷添加元素,而數(shù)組必須在初始化定義長(zhǎng)度,當(dāng)數(shù)組不足以存放更多的元素,就需要擴(kuò)大數(shù)組的長(zhǎng)度,方法是使用一個(gè)新的數(shù)組代替已有的數(shù)組.就像裝水時(shí)小桶換大桶.

擴(kuò)容步驟:

  • ①判斷舊的數(shù)組是否大于0,如果大于0且小于HashMap最大允許容量,則新的數(shù)組長(zhǎng)度為舊數(shù)組長(zhǎng)度*2
  • ②將舊數(shù)組的負(fù)載容量(長(zhǎng)度負(fù)載因子)2作為新數(shù)組的負(fù)載容量
  • ③創(chuàng)建一個(gè)新的Node數(shù)組,長(zhǎng)度為舊數(shù)組長(zhǎng)度*2
 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) {
        //如果舊的哈希桶長(zhǎng)度大于最大可值,則將最大負(fù)載設(shè)為Integer的最大值,返回舊哈希桶
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
        //將舊桶的大小左移1位,相當(dāng)于乘2,就是新桶的大小,同時(shí)新桶數(shù)量必須小于最大容量,并且舊桶長(zhǎng)度大于默認(rèn)容量
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
...

}
  • ④遍歷舊數(shù)組,判斷元素哈希值&舊數(shù)組長(zhǎng)度是否為0,如果為0則將元素放在原下標(biāo),如果不為0則key的新下標(biāo)的值等于原下標(biāo)+舊數(shù)組長(zhǎng)度.
    圖中n為舊數(shù)組長(zhǎng)度,key1為key在舊數(shù)組中的hash值,key2是key在新數(shù)組中hash計(jì)算后的值


    圖3-1 原key在新的哈希桶下標(biāo)位計(jì)算方法

    由圖中可以得知,只要判斷原key在高位新增的是0或是1,就能得到新的下標(biāo).


    圖3-2 key在新數(shù)組中下標(biāo)計(jì)算方法

源碼實(shí)現(xiàn)

final Node<K,V>[] resize() {
...
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            //計(jì)算原索引
                            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);
                        //放在原下標(biāo)
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        //放在新的下標(biāo),下標(biāo)位置=原下標(biāo)+舊數(shù)組成長(zhǎng)度
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
...
}

參考資料:
Java 8系列之重新認(rèn)識(shí)HashMap

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末淫半,一起剝皮案震驚了整個(gè)濱河市椎椰,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件仿吞,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡捡偏,警方通過(guò)查閱死者的電腦和手機(jī)唤冈,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)银伟,“玉大人你虹,你說(shuō)我怎么就攤上這事⊥埽” “怎么了傅物?”我有些...
    開(kāi)封第一講書人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)忠藤。 經(jīng)常有香客問(wèn)我挟伙,道長(zhǎng)楼雹,這世上最難降的妖魔是什么模孩? 我笑而不...
    開(kāi)封第一講書人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任尖阔,我火速辦了婚禮,結(jié)果婚禮上榨咐,老公的妹妹穿的比我還像新娘介却。我一直安慰自己,他們只是感情好块茁,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布齿坷。 她就那樣靜靜地躺著,像睡著了一般数焊。 火紅的嫁衣襯著肌膚如雪永淌。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 51,679評(píng)論 1 305
  • 那天佩耳,我揣著相機(jī)與錄音遂蛀,去河邊找鬼。 笑死干厚,一個(gè)胖子當(dāng)著我的面吹牛李滴,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蛮瞄,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼所坯,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了挂捅?” 一聲冷哼從身側(cè)響起芹助,我...
    開(kāi)封第一講書人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎闲先,沒(méi)想到半個(gè)月后周瞎,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡饵蒂,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年声诸,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片退盯。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡彼乌,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出渊迁,到底是詐尸還是另有隱情慰照,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布琉朽,位于F島的核電站毒租,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏箱叁。R本人自食惡果不足惜墅垮,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一惕医、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧算色,春花似錦抬伺、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至若河,卻和暖如春能岩,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背萧福。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工捧灰, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人统锤。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓毛俏,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親饲窿。 傳聞我的和親對(duì)象是個(gè)殘疾皇子煌寇,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355

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