HashMap JDK1.7 和JDK1.8

前言

本文主要講解HashMap的底層數(shù)據(jù)結(jié)構(gòu)锦援、存取原理蝗碎、擴(kuò)容機(jī)制湖笨、線程安全性、java 7 和java 8版本的對(duì)比等方面蹦骑。如果你正在學(xué)習(xí)HashMap慈省,希望對(duì)你有幫助。
如果你需要目錄眠菇,可以查看這篇關(guān)于簡(jiǎn)書(shū)生成目錄的文章边败。

.

簡(jiǎn)介


HashMap 是由數(shù)組和鏈表組合構(gòu)成的數(shù)據(jù)機(jī)構(gòu)袱衷。JDK1.7 和 JDK 1.8 的HashMap底層略有不同。

HashMap根據(jù)鍵的hashCode值存儲(chǔ)數(shù)據(jù)笑窜,大多數(shù)情況下可以直接定位到它的值致燥,具有很快的訪問(wèn)速度,但遍歷順序是不確定的排截。

HashMap最多只允許一條記錄的鍵為null嫌蚤,允許多條記錄的值為null。

HashMap是非線程安全的断傲。
.

底層數(shù)據(jù)結(jié)構(gòu)


HashMap 是數(shù)組 + 鏈表 + 紅黑樹(shù)(JDK1.8 新增了紅黑樹(shù)部分)構(gòu)成的脱吱。

結(jié)構(gòu)圖如下:

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

HashMap是一個(gè)存儲(chǔ)Key-Value鍵值對(duì)的集合,每一個(gè)鍵值對(duì)也稱(chēng)Entry(Java8中叫Node)艳悔,這些Entry分散存儲(chǔ)在一個(gè)數(shù)組中急凰,這個(gè)數(shù)組就是HashMap的主干。

HashMap_Node源碼.png

數(shù)組存放的是Node猜年,通過(guò)查看Node的源碼抡锈,可以發(fā)現(xiàn)每個(gè)節(jié)點(diǎn)都會(huì)保存自身的hash、key乔外、value 以及下一個(gè)節(jié)點(diǎn)的引用next床三。

之所以JDK1.8加入了紅黑樹(shù),是為了提高效率杨幼,因?yàn)樵贘DK1.7的時(shí)候只有鏈表撇簿,存放的數(shù)據(jù)一多就容易導(dǎo)致鏈表變長(zhǎng),降低了效率差购。

只有在鏈表的長(zhǎng)度不小于8四瘫,而且數(shù)組的長(zhǎng)度不小于64的時(shí)候才會(huì)將鏈表轉(zhuǎn)化為紅黑樹(shù)

.

存取原理


在JDK1.7的時(shí)候,HashMap存放數(shù)據(jù)采用的是頭插法欲逃,JDK1.8改為尾插法

static final int hash(Object key) {   //jdk1.8 & jdk1.7
     int h;
     // h = key.hashCode() 為第一步 取hashCode值
     // h ^ (h >>> 16)  為第二步 高位參與運(yùn)算
     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

static int indexFor(int hash, int length) {  //jdk1.7的源碼找蜜,jdk1.8沒(méi)有這個(gè)方法,但是實(shí)現(xiàn)原理一樣的
     return h & (length-1);  //第三步 取模運(yùn)算
}

.

采用頭插法(JDK1.7)

HashMap在put數(shù)據(jù)的時(shí)候會(huì)根據(jù) index = indexFor(hash, table.length) 計(jì)算出index值稳析,如:put("name", ''xiaoming')的時(shí)候就通過(guò)上面式子計(jì)算出 index = 2 洗做,那么這個(gè)鍵值對(duì)就存放到數(shù)組下標(biāo)為2的位置。

我們知道數(shù)組的長(zhǎng)度是有限的彰居,在有限的長(zhǎng)度里面使用哈希就會(huì)有概率兩個(gè)key都hash到一個(gè)值上诚纸,這個(gè)時(shí)候新來(lái)的key就會(huì)取代原位置上的key,原位置的key就順推到新來(lái)的key后面去陈惰,形成一個(gè)鏈表畦徘。

頭插法:新來(lái)的值會(huì)取代原有的值,原有的值順推到鏈表中去。

為什么這么設(shè)計(jì)旧烧?因?yàn)樽髡哒J(rèn)為后來(lái)的值被查找的可能性更大一些影钉,提升查找的效率。

.

確定key的存放位置(JDK1.7)

從上面我們知道HashMap是如何存儲(chǔ)數(shù)據(jù)的掘剪,接下來(lái)平委,我們需要了解HashMap是如何確定key在數(shù)組中的存放位置

我們知道 index 是通過(guò) indexFor(int hash, int length) 得到的夺谁,而hash值是通過(guò) hash(Object key) 得到的廉赔。那么如何使得得到的下標(biāo) index 在數(shù)組中均勻分布呢?

可以利用Key的hash值和HashMap的長(zhǎng)度做取模運(yùn)算匾鸥,但取模運(yùn)算效率低蜡塌,在HashMap中是這樣子來(lái)計(jì)算index的:index = hash(Key) & (length - 1)

當(dāng) length 總是 2? 時(shí)勿负,上面式子就等價(jià)于對(duì) length 取模馏艾。

2? - 1 用二進(jìn)制表示,所有位都是 1 奴愉,根據(jù) & 運(yùn)算的規(guī)則琅摩,可以知道Hash算法最終得到的index結(jié)果,完全取決于Key的HashCode值的最后幾位锭硼。

這里有個(gè)問(wèn)題:為什么HashMap的長(zhǎng)度必須是16或者是2的冪房资?

答:為了實(shí)現(xiàn)均勻分布。因?yàn)橹灰?的冪檀头,那么 Length - 1 的值的所有二進(jìn)制位都為1轰异,這種情況下index的值就等同于HashCode最后幾位的值。只要HashCode是分布均勻的暑始,Hash(Key) 的結(jié)果就是均勻的搭独。而其他數(shù)可能會(huì)導(dǎo)致有些index的結(jié)果出現(xiàn)幾率更大或有些index結(jié)果永遠(yuǎn)不會(huì)出現(xiàn)。

.

確定key的存放位置(JDK1.8)

JDK1.8的實(shí)現(xiàn)中廊镜,優(yōu)化了高位運(yùn)算的算法戳稽,通過(guò)hashCode()的高16位異或低16位(擾動(dòng)函數(shù))實(shí)現(xiàn)的:

(h = key.hashCode()) ^ (h >>> 16)

使用擾動(dòng)函數(shù)的目的:為了混合原始哈希碼的高位和低位,以此來(lái)加大低位隨機(jī)性期升,降低發(fā)生hash沖突的概率。

下面一個(gè)例子互躬,n為數(shù)組的長(zhǎng)度

HashMap_計(jì)算下標(biāo).png

.

擴(kuò)容機(jī)制


HashMap采用懶加載播赁,構(gòu)造完HashMap對(duì)象后并未初始化數(shù)組table,而是在第一調(diào)用 put() 時(shí)調(diào)用resize方法進(jìn)行初始化吼渡。

當(dāng)向HashMap容器添加元素的時(shí)候容为,會(huì)判斷當(dāng)前容器中的元素個(gè)數(shù),如果大于或等于閾(yù)值 ——當(dāng)前數(shù)組的長(zhǎng)度乘以負(fù)載因子(LoadFactor),就需要擴(kuò)容數(shù)組坎背,java里面的數(shù)組是無(wú)法動(dòng)態(tài)增加長(zhǎng)度的替劈,方法就是使用一個(gè)新的數(shù)組替換舊數(shù)組。新數(shù)組的長(zhǎng)度是原數(shù)組長(zhǎng)度的2倍得滤。

對(duì)于resize的源碼陨献,JDK1.7和JDK1.7是不同的,區(qū)別在于JDK1.8加入了紅黑樹(shù)懂更,更為復(fù)雜眨业,但本質(zhì)區(qū)別不大。

.

擴(kuò)容過(guò)程(JDK1.7)

jdk1.7的擴(kuò)容做法就是簡(jiǎn)單的創(chuàng)建一個(gè)新的Entry空數(shù)組(長(zhǎng)度是原數(shù)組的2倍)沮协,然后遍歷原數(shù)組龄捡,將所有的Entry重新Hash到新的數(shù)組中去,最后修改閾值慷暂。

轉(zhuǎn)移過(guò)程示例:

HashMap_鏈表轉(zhuǎn)移過(guò)程.png

為什么需要重新Hash呢聘殖?

答:因?yàn)殚L(zhǎng)度擴(kuò)大后,Hash的規(guī)則也隨之改變行瑞。Hash的公式index = HashCode(key) & (length - 1)

.

線程不安全性

為什么JDK1.7使用頭插法奸腺,JDK1.8使用尾插法?

JDK1.7在多線程操作HashMap時(shí)可能引起死循環(huán)蘑辑,原因是擴(kuò)容轉(zhuǎn)移后前后鏈表順序倒置洋机,在轉(zhuǎn)移過(guò)程中修改了原來(lái)鏈表中節(jié)點(diǎn)的引用關(guān)系。

JDK1.8在同樣的前提下不會(huì)引起死循環(huán)洋魂,原因是擴(kuò)容轉(zhuǎn)移后前后鏈表順序不變绷旗,保持之前節(jié)點(diǎn)的引用關(guān)系。

在容量為2的容器中用不同的線程插入A副砍、B衔肢、C,如果在resize之前打斷點(diǎn)豁翎,則意味著A角骤、B、C都插入了心剥,但是還未擴(kuò)容邦尊,那么有可能出現(xiàn)一種情況:在同一個(gè)index上的鏈表中是這樣子的: A —> B —> C

因?yàn)槭褂昧?strong>單鏈表的頭插入方式,同一位置上新元素總會(huì)被放在鏈表的頭部位置优烧,在舊數(shù)組中同一條Entry鏈上的元素蝉揍,通過(guò)重新計(jì)算索引位置后,有可能被放到了新數(shù)組的不同位置上畦娄。

也就是說(shuō)在resize的時(shí)候又沾,有可能出現(xiàn)這一種情況:在同一個(gè)index上弊仪, A先放進(jìn)來(lái),然后B后放進(jìn)來(lái)杖刷,就出現(xiàn)了這樣子的情況:B —> A —> B

可以看出來(lái)形成了一個(gè)環(huán)形鏈表了励饵,取值的時(shí)候需要遍歷鏈表,這個(gè)時(shí)候就會(huì)出現(xiàn)死循環(huán)滑燃。

而JDK1.8改用尾插法便可以解決這一個(gè)問(wèn)題役听。

HashMap 的線程不安全性主要體現(xiàn)在:

  • 在jdk1.7中,在多線程環(huán)境下不瓶,擴(kuò)容時(shí)會(huì)造成環(huán)形鏈或數(shù)據(jù)丟失禾嫉。

  • 在jdk1.8中,在多線程環(huán)境下蚊丐,會(huì)發(fā)生數(shù)據(jù)覆蓋的情況熙参。

雖然JDK1.8不會(huì)出現(xiàn)死循環(huán),當(dāng)是仍然不可以將HashMap用于多線程中麦备,put/get方法都是未加同步鎖的孽椰,容易出現(xiàn):上一秒put的值,下一秒get的時(shí)候還是原值凛篙,所以無(wú)法保證線程安全黍匾。

多線程環(huán)境下應(yīng)該使用 ConcurrentHashMap

.

擴(kuò)容過(guò)程(JDK1.8)

jdk1.8 引入紅黑樹(shù),因此擴(kuò)容過(guò)程多了對(duì)紅黑樹(shù)的處理呛梆,并且對(duì)重Hash做了優(yōu)化锐涯,比較復(fù)雜。

resize() 的源碼如下:

final Node<K,V>[] resize() {

    // 把當(dāng)前底層數(shù)組賦值給oldTab填物,為數(shù)據(jù)遷移工作做準(zhǔn)備 —— 拿到原數(shù)組的引用
    Node<K,V>[] oldTab = table;
    
    // 獲取當(dāng)前數(shù)組的大小纹腌,如果未初始化數(shù)組凡傅,則為0
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    
    // 獲取擴(kuò)容前的閾值(容量 * 負(fù)載因子)
    int oldThr = threshold;
    
    // 定義并初始化新數(shù)組長(zhǎng)度和目標(biāo)閾值
    int newCap, newThr = 0;
    
    // 判斷是初始化數(shù)組還是擴(kuò)容砌溺,等于或小于0表示需要初始化數(shù)組,大于0表示需要擴(kuò)容數(shù)組浴栽。
    // 若 oldCap > 0 表示需擴(kuò)容而非初始化
    if (oldCap > 0) {
        // 判斷數(shù)組長(zhǎng)度是否已經(jīng)是默認(rèn)最大击困,MAXIMUM_CAPACITY = 1 << 30 = 2^30
        // 超過(guò)默認(rèn)最大容量則不再進(jìn)行擴(kuò)容
        if (oldCap >= MAXIMUM_CAPACITY) {
            // 閾值設(shè)置為最大
            threshold = Integer.MAX_VALUE;
            return oldTab;
        } 
        // 如果新數(shù)組長(zhǎng)度擴(kuò)大2倍后小于最大容量涎劈,并且原數(shù)組容量大于等于默認(rèn)初始化容量(16)
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            // 目標(biāo)閾值擴(kuò)展2倍,數(shù)組長(zhǎng)度擴(kuò)展2倍
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0)
        // 說(shuō)明調(diào)用的是HashMap的有參構(gòu)造函數(shù)阅茶,因?yàn)闊o(wú)參構(gòu)造函數(shù)并沒(méi)有對(duì)threshold進(jìn)行初始化
        newCap = oldThr;
    // 表示需要初始化數(shù)組而不是擴(kuò)容蛛枚,零初始閾值表示使用默認(rèn)值
    else {
        // 說(shuō)明調(diào)用的是HashMap的無(wú)參構(gòu)造函數(shù)
        newCap = DEFAULT_INITIAL_CAPACITY;
        // 計(jì)算目標(biāo)閾值
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    // 當(dāng)目標(biāo)閾值為0時(shí)需重新計(jì)算,公式:容量(newCap)* 負(fù)載因子(loadFactor)
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    // 根據(jù)以上計(jì)算結(jié)果將閾值更新
    threshold = newThr;
    // 將新數(shù)組賦值給底層數(shù)組
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;

    // -------------------------------------------------------------------------------------
    // 此時(shí)已完成初始化數(shù)組或擴(kuò)容數(shù)組脸哀,但原數(shù)組內(nèi)的數(shù)據(jù)并未遷移至新數(shù)組(擴(kuò)容后的數(shù)組)
    // 之后的代碼則是完成原數(shù)組向新數(shù)組的數(shù)據(jù)遷移過(guò)程
    // -------------------------------------------------------------------------------------

    // 判斷原數(shù)組內(nèi)是否有存儲(chǔ)數(shù)據(jù)坤候,有的話開(kāi)始遷移數(shù)據(jù)
    if (oldTab != null) {
        // 開(kāi)始循環(huán)遷移數(shù)據(jù)
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            // 將數(shù)組內(nèi)此下標(biāo)中的數(shù)據(jù)賦值給Node類(lèi)型的變量e,并判斷非空
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                
                // 1 - 普通元素判斷:判斷數(shù)組內(nèi)此下標(biāo)中是否只存儲(chǔ)了一個(gè)元素企蹭,
                //                 是的話表示這是一個(gè)普通元素白筹,并開(kāi)始轉(zhuǎn)移
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                
                // 2 - 紅黑樹(shù)判斷:判斷此下標(biāo)內(nèi)是否是一顆紅黑樹(shù),是的話進(jìn)行數(shù)據(jù)遷移
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                
                // 3 - 鏈表判斷:若此下標(biāo)內(nèi)包含的數(shù)據(jù)既不是普通元素又不是紅黑樹(shù)谅摄,
                //              則它只能是一個(gè)鏈表徒河,進(jìn)行數(shù)據(jù)轉(zhuǎn)移
                else { // preserve order
                    // 之所以定義兩個(gè)頭兩個(gè)尾對(duì)象,是由于鏈表中的元素的下標(biāo)在擴(kuò)容后,
                    // 要么不變,要么是原下標(biāo)+oldCap
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        // 判斷元素是否需要改變索引送漠,從而分成兩個(gè)鏈表
                        // 一個(gè)存放位置不變的元素顽照,一個(gè)存放位置需要改變的元素
                        // 注意:位置需要改變的那個(gè)鏈表中的元素在新數(shù)組中的index是一樣的
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                // 第一個(gè)節(jié)點(diǎn)
                                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;
                        // 在新數(shù)組中位置與原來(lái)的一樣
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        // 在新數(shù)組中的位置 = 原位置偏移原數(shù)組的長(zhǎng)度
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    // 返回初始化完成或擴(kuò)容完成的新數(shù)組
    return newTab;
}

.

JDK1.8 對(duì)重Hash的優(yōu)化

回顧一下jdk1.8的計(jì)算key在數(shù)組的下標(biāo)的方式:獲取key的hashCode,然后將hashCode的16位異或低16位獲得hash闽寡,最后將 hash & (n - 1) 得到 index代兵,如果將原數(shù)組轉(zhuǎn)移到新數(shù)組中都需要這樣子重新計(jì)算Hash,那對(duì)整體的性能肯定有影響爷狈。

通過(guò)觀察轉(zhuǎn)移過(guò)程可以發(fā)現(xiàn)植影,每次擴(kuò)容后數(shù)組的長(zhǎng)度都是原來(lái)的2倍,也就是說(shuō)涎永,數(shù)組的長(zhǎng)度是 2? 思币,所以,元素的新位置要么是在原位置羡微,要么是在原來(lái)的位置上移動(dòng)原數(shù)組長(zhǎng)度的位置谷饿。

上面的源碼是通過(guò) if ((e.hash & oldCap) == 0) 來(lái)將原鏈表分成兩個(gè),一個(gè)存位置不需要改變的元素妈倔,一個(gè)存位置需要改變的元素博投,然后遍歷完一個(gè)index下的鏈表后,再將兩個(gè)鏈表分別移到新數(shù)組當(dāng)中去盯蝴。即源碼中的 newTab[j] = loHead;newTab[j + oldCap] = hiHead;

看下圖毅哗,其中 hash1 是 key1 對(duì)應(yīng)的 hashCode 與高位運(yùn)算的結(jié)果:

HashMap_重hash優(yōu)化計(jì)算index.png

圖中 n是數(shù)組的長(zhǎng)度,擴(kuò)容后 n 會(huì)變成原來(lái)的2倍结洼,就相當(dāng)于在原來(lái)的 n-1 的二進(jìn)制表示中的高位多1bit(紅色)黎做,因此新的index不必要重新計(jì)算hash,只需要看 n - 1 新增的那個(gè)1bit位對(duì)應(yīng)的 key 的hash的那個(gè)位是 0 還是 1松忍,如果是0蒸殿,那么index不變,如果是1鸣峭,那么index = 原來(lái)的index + 原來(lái)數(shù)組的長(zhǎng)度宏所。

看下圖:

HashMap_確定index是否需要改變.png

(e.hash & oldCap) == 0 為什么是oldCap 而不是 oldCao - 1 ?

假設(shè)原數(shù)組長(zhǎng)度為16摊溶,那么 oldCap 的二進(jìn)制為 1 0000 爬骤, oldCap - 1 的二進(jìn)制為 0 1111

我們需要判斷的是長(zhǎng)度擴(kuò)大后那個(gè)新增位是 0 還是 1

而 oldCap 的二進(jìn)制有一個(gè)1并且這個(gè)1對(duì)應(yīng)上了那個(gè)新增位,此時(shí)進(jìn)行 & 運(yùn)算就可以得知新增位是0還是1

.

重寫(xiě) equals() 必須重寫(xiě) hashCode()


在java中莫换,所有類(lèi)的祖先類(lèi)都是 Object霞玄,Object類(lèi)中有兩個(gè)方法 equalshashCode骤铃,這兩個(gè)方法都可以用來(lái)比較兩個(gè)對(duì)象是否相等。

對(duì)于值類(lèi)型坷剧,== 比較的是兩個(gè)變量的值惰爬,而對(duì)于引用類(lèi)型,== 比較的是兩個(gè)變量的地址惫企。

Object的equals方法源碼:

// 可以看到Object類(lèi)的equals方法比較的是地址撕瞧。
public boolean equals(Object obj) {
        return (this == obj);
}

hashCode()方法源碼中有一段注釋?zhuān)?/p>

在 Java 應(yīng)用程序執(zhí)行期間,在對(duì)同一對(duì)象多次調(diào)用 hashCode 方法時(shí)狞尔,必須一致地返回相同的整數(shù)丛版,前提是將對(duì)象進(jìn)行 equals 比較時(shí)所用的信息沒(méi)有被修改。從某一應(yīng)用程序的一次執(zhí)行到同一應(yīng)用程序的另一次執(zhí)行偏序,該整數(shù)無(wú)需保持一致页畦。

如果根據(jù) equals(Object) 方法,兩個(gè)對(duì)象是相等的禽车,那么對(duì)這兩個(gè)對(duì)象中的每個(gè)對(duì)象調(diào)用 hashCode 方法都必須生成相同的整數(shù)結(jié)果寇漫。

如果根據(jù) equals(java.lang.Object) 方法,兩個(gè)對(duì)象不相等殉摔,那么對(duì)這兩個(gè)對(duì)象中的任一對(duì)象上調(diào)用 hashCode 方法不要求一定生成不同的整數(shù)結(jié)果州胳。但是,程序員應(yīng)該意識(shí)到逸月,為不相等的對(duì)象生成不同整數(shù)結(jié)果可以提高哈希表的性能栓撞。

所以,源碼的注釋來(lái)看碗硬,兩方法同時(shí)重寫(xiě)是很必要的瓤湘。

前提條件,當(dāng)我們?cè)谑褂肏ashMap恩尾、HashSet等集合時(shí):

如果重寫(xiě)了equals方法而不重寫(xiě)hashCode方法弛说,那么就會(huì)出現(xiàn)通過(guò)equals方法比較相等的兩個(gè)對(duì)象因hashCode不同,而被存到HashMap中數(shù)組的不同index下(我們知道key的index是根據(jù)hashCode計(jì)算而來(lái)的)翰意,這就會(huì)導(dǎo)致HashMap存了2個(gè)一樣的key木人,那么在調(diào)用HashMap方法的時(shí)候就會(huì)出現(xiàn)邏輯錯(cuò)誤。

.

TODO:


四個(gè)構(gòu)造方法的分析

put/get 方法源碼研讀

擴(kuò)容時(shí)紅黑樹(shù)的轉(zhuǎn)移過(guò)程

.

常見(jiàn)面試題


HashMap的底層數(shù)據(jù)結(jié)構(gòu)冀偶?

hash沖突是什么醒第?解決hash沖突常見(jiàn)的方法有哪些?HashMap是如何解決hash沖突的进鸠?

HashMap是如何計(jì)算hash的稠曼?為什么要用hashCode的高16位異或低16位(擾動(dòng)函數(shù))?

HashMap的存取原理客年?

HashMap的默認(rèn)初始化大小是多少霞幅?為什么是這么多漠吻?為什么大小都是 2? ?

HashMap的負(fù)載因子是多少蝗岖?為什么是這么多侥猩?

說(shuō)一下HashMap的擴(kuò)容機(jī)制?什么時(shí)候進(jìn)行擴(kuò)容抵赢?擴(kuò)容的過(guò)程是怎么樣的?

HashMap為什么線程不安全唧取?有什么有可替代的集合铅鲤?

Java 7 和 Java 8 的區(qū)別是什么?Java 8 為什么要增加紅黑樹(shù)枫弟?鏈表何時(shí)轉(zhuǎn)為紅黑樹(shù)邢享?

Java 7 為什么使用頭插法而java8改用尾插法?

Java 8 對(duì)重hash的優(yōu)化是怎么樣的淡诗?為什么是oldCap 而不是 oldCao - 1 骇塘?

.

鳴謝


在學(xué)習(xí)HashMap底層原理的時(shí)候,拜讀了以下文章韩容,收獲良多款违,在此感謝一下作者!

Java 8系列之重新認(rèn)識(shí)HashMap

阿里面試官?zèng)]想到一個(gè)HashMap群凶,我能跟他扯半小時(shí)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末插爹,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子请梢,更是在濱河造成了極大的恐慌赠尾,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,692評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件毅弧,死亡現(xiàn)場(chǎng)離奇詭異气嫁,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)够坐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)寸宵,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人咆霜,你說(shuō)我怎么就攤上這事邓馒。” “怎么了蛾坯?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,995評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵光酣,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我脉课,道長(zhǎng)救军,這世上最難降的妖魔是什么财异? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,223評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮唱遭,結(jié)果婚禮上戳寸,老公的妹妹穿的比我還像新娘。我一直安慰自己拷泽,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,245評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布司致。 她就那樣靜靜地躺著拆吆,像睡著了一般。 火紅的嫁衣襯著肌膚如雪脂矫。 梳的紋絲不亂的頭發(fā)上枣耀,一...
    開(kāi)封第一講書(shū)人閱讀 51,208評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音庭再,去河邊找鬼捞奕。 笑死,一個(gè)胖子當(dāng)著我的面吹牛拄轻,可吹牛的內(nèi)容都是我干的颅围。 我是一名探鬼主播,決...
    沈念sama閱讀 40,091評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼哺眯,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼谷浅!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起奶卓,我...
    開(kāi)封第一講書(shū)人閱讀 38,929評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤一疯,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后夺姑,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體墩邀,經(jīng)...
    沈念sama閱讀 45,346評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,570評(píng)論 2 333
  • 正文 我和宋清朗相戀三年盏浙,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了眉睹。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,739評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡废膘,死狀恐怖竹海,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情丐黄,我是刑警寧澤斋配,帶...
    沈念sama閱讀 35,437評(píng)論 5 344
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響艰争,放射性物質(zhì)發(fā)生泄漏坏瞄。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,037評(píng)論 3 326
  • 文/蒙蒙 一甩卓、第九天 我趴在偏房一處隱蔽的房頂上張望鸠匀。 院中可真熱鬧,春花似錦逾柿、人聲如沸缀棍。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,677評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)睦柴。三九已至,卻和暖如春毡熏,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背侣诵。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,833評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工痢法, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人杜顺。 一個(gè)月前我還...
    沈念sama閱讀 47,760評(píng)論 2 369
  • 正文 我出身青樓财搁,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親躬络。 傳聞我的和親對(duì)象是個(gè)殘疾皇子尖奔,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,647評(píng)論 2 354