問:簡單說說 JDK 1.8 中 HashMap 是如何擴(kuò)容的?與 JDK 1.7 有什么區(qū)別?
答:JDK 1.7 中 HashMap 的擴(kuò)容機(jī)制簡單總結(jié)如下圖:

可以看見,1.7 中整個擴(kuò)容過程就是一個取出數(shù)組元素(實際數(shù)組索引位置上的每個元素是每個獨立單向鏈表的頭部,也就是發(fā)生 Hash 沖突后最后放入的沖突元素)然后遍歷以該元素為頭的單向鏈表元素,依據(jù)每個被遍歷元素的 hash 值計算其在新數(shù)組中的下標(biāo)然后進(jìn)行交換(即原來 hash 沖突的單向鏈表尾部變成了擴(kuò)容后單向鏈表的頭部)。
而在 JDK 1.8 中 HashMap 的擴(kuò)容操作就顯得更加的騷氣了,由于擴(kuò)容數(shù)組的長度是 2 倍關(guān)系,所以對于假設(shè)初始 tableSize = 4 要擴(kuò)容到 8 來說就是 0100 到 1000 的變化(左移一位就是 2 倍),在擴(kuò)容中只用判斷原來的 hash 值與左移動的一位(newtable 的值)按位與操作是 0 或 1 就行,0 的話索引就不變,1 的話索引變成原索引加上擴(kuò)容前數(shù)組,所以其實現(xiàn)如下流程圖所示:

上圖就是 1.8 與 1.7 擴(kuò)容的核心流程圖區(qū)別,其 1.8 源碼核心實現(xiàn)如下:
final Node<K, V>[] resize () {
Node<K, V>[] oldTab = table;
//記住擴(kuò)容前的數(shù)組長度和最大容量
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
//超過數(shù)組在java中最大容量就無能為力了,沖突就只能沖突
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//長度和最大容量都擴(kuò)容為原來的二倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1;
double threshold
}
//...... ......
//更新新的最大容量為擴(kuò)容計算后的最大容量
threshold = newThr;
//更新擴(kuò)容后的新數(shù)組長度
Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];
table = newTab;
if (oldTab != null) {
//遍歷老數(shù)組下標(biāo)索引
for (int j = 0; j < oldCap; ++j) {
Node<K, V> e;
//如果老數(shù)組對應(yīng)索引上有元素則取出鏈表頭元素放在e中
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//如果老數(shù)組j下標(biāo)處只有一個元素則直接計算新數(shù)組中位置放置
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
//如果是樹結(jié)構(gòu)進(jìn)行單獨處理
((TreeNode<K, V>) e).split(this, newTab, j, oldCap);
else {
preserve order
//能進(jìn)來說明數(shù)組索引j位置上存在哈希沖突的鏈表結(jié)構(gòu)
Node<K, V> loHead = null, loTail = null;
Node<K, V> hiHead = null, hiTail = null;
Node<K, V> next;
//循環(huán)處理數(shù)組索引j位置上哈希沖突的鏈表中每個元素
do {
next = e.next;
//判斷key的hash值與老數(shù)組長度與操作后結(jié)果決定元素是放在原索引處還是新索引
if ((e.hash & oldCap) == 0) {
//放在原索引處的建立新鏈表
if (loTail == null) loHead = e;
else loTail.next = e;
loTail = e;
} else {
//放在新索引(原索引 + 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;
}
}
}
}
}
return newTab;
}
可以看見,這個設(shè)計非常贊,因為 hash 值本來就是隨機(jī)性的,所以 hash 按位與上 newTable 得到的 0(擴(kuò)容前的索引位置)和 1(擴(kuò)容前索引位置加上擴(kuò)容前數(shù)組長度的數(shù)值索引處)就是隨機(jī)的,所以擴(kuò)容的過程就能把之前哈希沖突的元素再隨機(jī)的分布到不同的索引去,這算是 JDK1.8 的一個優(yōu)化點。
此外,在 JDK1.7 中擴(kuò)容操作時,哈希沖突的數(shù)組索引處的舊鏈表元素擴(kuò)容到新數(shù)組時,如果擴(kuò)容后索引位置在新數(shù)組的索引位置與原數(shù)組中索引位置相同,則鏈表元素會發(fā)生倒置(即如上面圖1,原來鏈表頭擴(kuò)容后變?yōu)槲舶停?;而?JDK1.8 中不會出現(xiàn)鏈表倒置現(xiàn)象。
其次,由于 JDK1.7 中發(fā)生哈希沖突時僅僅采用了鏈表結(jié)構(gòu)存儲沖突元素,所以擴(kuò)容時僅僅是重新計算其存儲位置而已,而 JDK1.8 中為了性能在同一索引處發(fā)生哈希沖突到一定程度時鏈表結(jié)構(gòu)會轉(zhuǎn)換為紅黑數(shù)結(jié)構(gòu)存儲沖突元素,故在擴(kuò)容時如果當(dāng)前索引中元素結(jié)構(gòu)是紅黑樹且元素個數(shù)小于鏈表還原閾值(哈希沖突程度常量)時就會把樹形結(jié)構(gòu)縮小或直接還原為鏈表結(jié)構(gòu)(其實現(xiàn)就是上面代碼片段中的 split() 方法)。