前言
本文主要講解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是一個(gè)存儲(chǔ)Key-Value鍵值對(duì)的集合,每一個(gè)鍵值對(duì)也稱(chēng)Entry(Java8中叫Node)艳悔,這些Entry分散存儲(chǔ)在一個(gè)數(shù)組中急凰,這個(gè)數(shù)組就是HashMap的主干。
數(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)度
.
擴(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ò)程示例:
為什么需要重新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é)果:
圖中 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)度宏所。
看下圖:
(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è)方法 equals
和 hashCode
骤铃,這兩個(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í)候,拜讀了以下文章韩容,收獲良多款违,在此感謝一下作者!