JDK1.8開始HashMap為什么要先插入后擴(kuò)容,網(wǎng)上查找有說先擴(kuò)容再插入可以少遍歷之類的,其實(shí)不管是先擴(kuò)容還是先插入,它的原則還是尾插法都是避免不了要遍歷的,那它為什么還是要先插入呢,只要看插入邏輯和擴(kuò)充邏輯做了哪些操作就知道了,以下也 只是個(gè)人的理解,如有錯(cuò)誤歡迎指點(diǎn)
首先看下JDK1.8 HashMap插入的源碼
1:插入操作如果數(shù)組中的節(jié)點(diǎn)是紅黑樹是往節(jié)點(diǎn)中插入節(jié)點(diǎn), 如果是鏈表的時(shí)候可能會要從鏈表升級成紅黑樹,似乎先插入再擴(kuò)容還是先擴(kuò)容后插入都是沒影響的都是要遍歷, 那問題原因就在擴(kuò)容機(jī)制里
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.next = newNode(hash, key, value, null);
/**
* 這里要滿足當(dāng)前鏈表的長度>=7,遍歷是從根節(jié)點(diǎn)開始,因此相當(dāng)于包括根節(jié)點(diǎn)有8個(gè)時(shí)再插入就會調(diào)用此方法
*/
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
2:再看看擴(kuò)容的源碼中有個(gè)關(guān)鍵的代碼((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
if (loHead != null) {
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
if (hiHead != null) {
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead;
if (loHead != null)
hiHead.treeify(tab);
}
}
擴(kuò)容的時(shí)候如果紅黑樹節(jié)點(diǎn)的個(gè)數(shù)<=6個(gè)時(shí),就會降級成鏈表了
HashMap為何將紅黑樹降級鏈表的閾值設(shè)置成6而不是7, 就是防止節(jié)點(diǎn)在紅黑樹與鏈表之間因插入擴(kuò)容而頻繁的轉(zhuǎn)變,頻繁的轉(zhuǎn)變是要消耗性能,而插入時(shí)的操作中將紅黑樹變成鏈表的觸發(fā)條件就是擴(kuò)容
下面就舉例頻繁轉(zhuǎn)變臨界點(diǎn)的情況 ,根據(jù)頻繁轉(zhuǎn)變臨界點(diǎn)的情況對比來說明原因,假設(shè)插入的節(jié)點(diǎn)剛好都在當(dāng)前的鏈表或者紅黑樹的索引位置上
臨界點(diǎn)情形一
假設(shè)當(dāng)前插入節(jié)點(diǎn)是紅黑樹結(jié)構(gòu)有7個(gè), 如果先擴(kuò)容后會有1個(gè)節(jié)點(diǎn)移到別的index索引時(shí),導(dǎo)致當(dāng)前紅黑樹的節(jié)點(diǎn)變成6個(gè)就會降級成鏈表再插入變成7個(gè)節(jié)點(diǎn)的鏈表。
而如果是先插入的話就是8個(gè)節(jié)點(diǎn)紅黑樹,然后擴(kuò)容即使有1個(gè)節(jié)點(diǎn)移到別的位置也還是7個(gè)節(jié)點(diǎn)也不會轉(zhuǎn)變成鏈表
臨界點(diǎn)情形二
和情形一邏輯相反的情況,如果當(dāng)前插入節(jié)點(diǎn)為8個(gè)且為鏈表的時(shí)候,如果先擴(kuò)容后會有1個(gè)節(jié)點(diǎn)移到別的index索引時(shí)再插入還是8個(gè)節(jié)點(diǎn)鏈表不會變紅黑樹,而如果是先插入就會變成紅黑樹再擴(kuò)容有1節(jié)點(diǎn)移動(dòng)時(shí)變成8個(gè)節(jié)點(diǎn)的紅黑樹,如果此時(shí)移除節(jié)點(diǎn)又有可能降成鏈表,先分析下移除節(jié)點(diǎn)時(shí)降鏈表?xiàng)l件的源碼
//這是移除時(shí)的部分源碼
if (root == null || root.right == null || (rl = root.left) == null || rl.left == null) {
tab[index] = first.untreeify(map); // too small
return;
}
可以看出在移除節(jié)點(diǎn)的時(shí)候不一定是6個(gè)節(jié)點(diǎn)就會導(dǎo)致紅黑樹降級成鏈表,根據(jù)紅黑樹的特點(diǎn)有可能當(dāng)前鏈表 3 <= Nodes <=6時(shí)才會降級成鏈表
這里也給一個(gè)模擬HashMap造出紅黑樹的測試代碼,紅黑樹為7個(gè)節(jié)點(diǎn)的時(shí)候,從64擴(kuò)容到128時(shí)導(dǎo)致了key為64的節(jié)點(diǎn)移到第64的索引中去,而紅黑樹就變成了鏈表了
public static void main(String[] args) {
//默認(rèn)64個(gè)大小hashMap,只要數(shù)組長度>=64, 且鏈表 》=8時(shí)時(shí)才會轉(zhuǎn)紅黑樹
HashMap<Integer, String> map = new HashMap<>(64);
map.put(0, "王二麻子 0");
map.put(64, "王二麻子 1");
map.put(128, "王二麻子 2");
map.put(256, "王二麻子 3");
map.put(512, "王二麻子 4");
map.put(1024, "王二麻子 5");
map.put(2048, "王二麻子 6");
map.put(4096, "王二麻子 7");
map.put(8192, "王二麻子 8");
//以上步驟就會生成在第0的索引上的一個(gè)9個(gè)節(jié)點(diǎn)的紅黑樹結(jié)構(gòu)
map.remove(8192);
map.remove(4096);
//移除二個(gè)后此時(shí)是一個(gè)有7個(gè)節(jié)點(diǎn)的紅黑樹
//這一步添加41個(gè)其他位置上的節(jié)點(diǎn),只要不在0節(jié)點(diǎn)上就行,讓節(jié)點(diǎn)總數(shù)達(dá)到 64*0.75 = 48個(gè)
for (int i = 1; i <= 41; i++) {
map.put(i, "張三 " + i);
}
//插入第49個(gè)時(shí)觸發(fā)擴(kuò)容,此后紅黑樹結(jié)構(gòu)就變成鏈表結(jié)構(gòu)了
map.put(42, "李四");
}