前言
JDK 1.8 對 HashMap 進行了比較大的優化,底層實現由之前的 “數組+鏈表” 改爲 “數組+鏈表+紅黑樹”,本文就 HashMap 的幾個常用的重要方法和 JDK 1.8 之前的死循環問題展開學習討論。
JDK 1.8 的 HashMap 的數據結構如下圖所示,當鏈表節點較少時仍然是以鏈表存在,當鏈表節點較多時(大於8)會轉爲紅黑樹。
本文地址:http://blog.csdn.net/v123411739/article/details/78996181
幾個點:
先了解以下幾個點,有利於更好的理解 HashMap 的源碼和閱讀本文。
1、本文中頭節點指的是 table 表上索引位置的節點,也就是鏈表的頭節點。
2、根節點(root 節點)指的是紅黑樹最上面的那個節點,也就是沒有父節點的節點。
3、紅黑樹的根節點不一定是索引位置的頭節點(也就是鏈表的頭節點),HashMap 通過 moveRootToFront 方法來維持紅黑樹的根結點就是索引位置的頭結點,但是在 removeTreeNode 方法中,當 movable 爲 false 時,不會調用 moveRootToFront 方法,此時紅黑樹的根節點不一定是索引位置的頭節點,該場景發生在 HashIterator 的 remove 方法中。
4、轉爲紅黑樹節點後,鏈表的結構還存在,通過 next 屬性維持,紅黑樹節點在進行操作時都會維護鏈表的結構,並不是轉爲紅黑樹節點,鏈表結構就不存在了。
5、在紅黑樹上,葉子節點也可能有 next 節點,因爲紅黑樹的結構跟鏈表的結構是互不影響的,不會因爲是葉子節點就說該節點已經沒有 next 節點。
6、源碼中一些變量定義:如果定義了一個節點 p,則 pl(p left)爲 p 的左節點,pr(p right)爲 p 的右節點,pp(p parent)爲 p 的父節點,ph(p hash)爲 p 的 hash 值,pk(p key)爲 p 的 key 值,kc(key class)爲 key 的類等等。源碼中很喜歡在 if/for 等語句中進行賦值並判斷,請注意。
7、鏈表中移除一個節點只需如下圖操作,其他操作同理。
8、紅黑樹在維護鏈表結構時,移除一個節點只需如下圖操作(紅黑樹中增加了一個 prev 屬性),其他操作同理。注:此處只是紅黑樹維護鏈表結構的操作,紅黑樹還需要單獨進行紅黑樹的移除或者其他操作。
9、源碼中進行紅黑樹的查找時,會反覆用到以下兩條規則:1)如果目標節點的 hash 值小於 p 節點的 hash 值,則向 p 節點的左邊遍歷;否則向 p 節點的右邊遍歷。2)如果目標節點的 key 值小於 p 節點的 key 值,則向 p 節點的左邊遍歷;否則向 p 節點的右邊遍歷。這兩條規則是利用了紅黑樹的特性(左節點 < 根節點 < 右節點)。
10、源碼中進行紅黑樹的查找時,會用 dir(direction)來表示向左還是向右查找,dir 存儲的值是目標節點的 hash/key 與 p 節點的 hash/key 的比較結果。
基本屬性
// 默認容量16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// 最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
// 默認負載因子0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 鏈表節點轉換紅黑樹節點的閾值, 9個節點轉
static final int TREEIFY_THRESHOLD = 8;
// 紅黑樹節點轉換鏈表節點的閾值, 6個節點轉
static final int UNTREEIFY_THRESHOLD = 6;
// 轉紅黑樹時, table的最小長度
static final int MIN_TREEIFY_CAPACITY = 64;
// 鏈表節點, 繼承自Entry
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
// ... ...
}
// 紅黑樹節點
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
// ...
}
定位哈希桶數組索引位置
不管增加、刪除、查找鍵值對,定位到哈希桶數組的位置都是很關鍵的第一步。前面說過 HashMap 的數據結構是“數組+鏈表+紅黑樹”的結合,所以我們當然希望這個 HashMap 裏面的元素位置儘量分佈均勻些,儘量使得每個位置上的元素數量只有一個,那麼當我們用 hash 算法求得這個位置的時候,馬上就可以知道對應位置的元素就是我們要的,不用遍歷鏈表/紅黑樹,大大優化了查詢的效率。HashMap 定位數組索引位置,直接決定了 hash 方法的離散性能。下面是定位哈希桶數組的源碼:
// 代碼1
static final int hash(Object key) { // 計算key的hash值
int h;
// 1.先拿到key的hashCode值; 2.將hashCode的高16位參與運算
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// 代碼2
int n = tab.length;
// 將(tab.length - 1) 與 hash值進行&運算
int index = (n - 1) & hash;
整個過程本質上就是三步:
拿到 key 的 hashCode 值
將 hashCode 的高位參與運算,重新計算 hash 值
將計算出來的 hash 值與 (table.length - 1) 進行 & 運算
方法解讀:
對於任意給定的對象,只要它的 hashCode() 返回值相同,那麼計算得到的 hash 值總是相同的。我們首先想到的就是把 hash 值對 table 長度取模運算,這樣一來,元素的分佈相對來說是比較均勻的。
但是模運算消耗還是比較大的,我們知道計算機比較快的運算爲位運算,因此 JDK 團隊對取模運算進行了優化,使用上面代碼2的位與運算來代替模運算。這個方法非常巧妙,它通過 “(table.length -1) & h” 來得到該對象的索引位置,這個優化是基於以下公式:x mod 2^n = x & (2^n - 1)。我們知道 HashMap 底層數組的長度總是 2 的 n 次方,並且取模運算爲 “h mod table.length”,對應上面的公式,可以得到該運算等同於“h & (table.length - 1)”。這是 HashMap 在速度上的優化,因爲 & 比 % 具有更高的效率。
在 JDK1.8 的實現中,還優化了高位運算的算法,將 hashCode 的高 16 位與 hashCode 進行異或運算,主要是爲了在 table 的 length 較小的時候,讓高位也參與運算,並且不會有太大的開銷。
下圖是一個簡單的例子:
當 table 長度爲 16 時,table.length - 1 = 15 ,用二進制來看,此時低 4 位全是 1,高 28 位全是 0,與 0 進行 & 運算必然爲 0,因此此時 hashCode 與 “table.length - 1” 的 & 運算結果只取決於 hashCode 的低 4 位,在這種情況下,hashCode 的高 28 位就沒有任何作用,並且由於 hash 結果只取決於 hashCode 的低 4 位,hash 衝突的概率也會增加。因此,在 JDK 1.8 中,將高位也參與計算,目的是爲了降低 hash 衝突的概率。
get 方法
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
// 1.對table進行校驗:table不爲空 && table長度大於0 &&
// table索引位置(使用table.length - 1和hash值進行位與運算)的節點不爲空
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
// 2.檢查first節點的hash值和key是否和入參的一樣,如果一樣則first即爲目標節點,直接返回first節點
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 3.如果first不是目標節點,並且first的next節點不爲空則繼續遍歷
if ((e = first.next) != null) {
if (first instanceof TreeNode)
// 4.如果是紅黑樹節點,則調用紅黑樹的查找目標節點方法getTreeNode
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
// 5.執行鏈表節點的查找,向下遍歷鏈表, 直至找到節點的key和入參的key相等時,返回該節點
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
// 6.找不到符合的返回空
return null;
}
4.如果是紅黑樹節點,則調用紅黑樹的查找目標節點方法 getTreeNode,見代碼塊1詳解。
代碼塊1:getTreeNode
final TreeNode<K,V> getTreeNode(int h, Object k) {
// 1.首先找到紅黑樹的根節點;2.使用根節點調用find方法
return ((parent != null) ? root() : this).find(h, k, null);
}
2.使用根節點調用 find 方法,見代碼塊2詳解。
代碼塊2:find
/**
* 從調用此方法的節點開始查找, 通過hash值和key找到對應的節點
* 此方法是紅黑樹節點的查找, 紅黑樹是特殊的自平衡二叉查找樹
* 平衡二叉查找樹的特點:左節點<根節點<右節點
*/
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
// 1.將p節點賦值爲調用此方法的節點,即爲紅黑樹根節點
TreeNode<K,V> p = this;
// 2.從p節點開始向下遍歷
do {
int ph, dir; K pk;
TreeNode<K,V> pl = p.left, pr = p.right, q;
// 3.如果傳入的hash值小於p節點的hash值,則往p節點的左邊遍歷
if ((ph = p.hash) > h)
p = pl;
else if (ph < h) // 4.如果傳入的hash值大於p節點的hash值,則往p節點的右邊遍歷
p = pr;
// 5.如果傳入的hash值和key值等於p節點的hash值和key值,則p節點爲目標節點,返回p節點
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
else if (pl == null) // 6.p節點的左節點爲空則將向右遍歷
p = pr;
else if (pr == null) // 7.p節點的右節點爲空則向左遍歷
p = pl;
// 8.將p節點與k進行比較
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) && // 8.1 kc不爲空代表k實現了Comparable
(dir = compareComparables(kc, k, pk)) != 0)// 8.2 k<pk則dir<0, k>pk則dir>0
// 8.3 k<pk則向左遍歷(p賦值爲p的左節點), 否則向右遍歷
p = (dir < 0) ? pl : pr;
// 9.代碼走到此處, 代表key所屬類沒有實現Comparable, 直接指定向p的右邊遍歷
else if ((q = pr.find(h, k, kc)) != null)
return q;
// 10.代碼走到此處代表“pr.find(h, k, kc)”爲空, 因此直接向左遍歷
else
p = pl;
} while (p != null);
return null;
}
8.將 p 節點與 k 進行比較。如果傳入的 key(即代碼中的參數 k)所屬的類實現了 Comparable 接口(kc 不爲空,comparableClassFor 方法見代碼塊3詳解),則將 k 跟 p 節點的 key 進行比較(kc 實現了 Comparable 接口,因此通過 kc 的比較方法進行比較),並將比較結果賦值給 dir,如果 dir<0 則代表 k<pk,則向 p 節點的左邊遍歷(pl);否則,向 p 節點的右邊遍歷(pr)。
代碼塊3:comparableClassFor
static Class<?> comparableClassFor(Object x) {
// 1.判斷x是否實現了Comparable接口
if (x instanceof Comparable) {
Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
// 2.校驗x是否爲String類型
if ((c = x.getClass()) == String.class) // bypass checks
return c;
if ((ts = c.getGenericInterfaces()) != null) {
// 3.遍歷x實現的所有接口
for (int i = 0; i < ts.length; ++i) {
// 4.如果x實現了Comparable接口,則返回x的Class
if (((t = ts[i]) instanceof ParameterizedType) &&
((p = (ParameterizedType)t).getRawType() ==
Comparable.class) &&
(as = p.getActualTypeArguments()) != null &&
as.length == 1 && as[0] == c) // type arg is c
return c;
}
}
}
return null;
}
put 方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 1.校驗table是否爲空或者length等於0,如果是則調用resize方法進行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 2.通過hash值計算索引位置,將該索引位置的頭節點賦值給p,如果p爲空則直接在該索引位置新增一個節點即可
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// table表該索引位置不爲空,則進行查找
Node<K,V> e; K k;
// 3.判斷p節點的key和hash值是否跟傳入的相等,如果相等, 則p節點即爲要查找的目標節點,將p節點賦值給e節點
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 4.判斷p節點是否爲TreeNode, 如果是則調用紅黑樹的putTreeVal方法查找目標節點
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 5.走到這代表p節點爲普通鏈表節點,則調用普通的鏈表方法進行查找,使用binCount統計鏈表的節點數
for (int binCount = 0; ; ++binCount) {
// 6.如果p的next節點爲空時,則代表找不到目標節點,則新增一個節點並插入鏈表尾部
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 7.校驗節點數是否超過8個,如果超過則調用treeifyBin方法將鏈表節點轉爲紅黑樹節點,
// 減一是因爲循環是從p節點的下一個節點開始的
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
// 8.如果e節點存在hash值和key值都與傳入的相同,則e節點即爲目標節點,跳出循環
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e; // 將p指向下一個節點
}
}
// 9.如果e節點不爲空,則代表目標節點存在,使用傳入的value覆蓋該節點的value,並返回oldValue
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e); // 用於LinkedHashMap
return oldValue;
}
}
++modCount;
// 10.如果插入節點後節點數超過閾值,則調用resize方法進行擴容
if (++size > threshold)
resize();
afterNodeInsertion(evict); // 用於LinkedHashMap
return null;
}
1.校驗 table 是否爲空或者 length 等於0,如果是則調用 resize 方法進行初始化,見resize方法詳解。
4.如果 p 節點不是目標節點,則判斷 p 節點是否爲 TreeNode,如果是則調用紅黑樹的 putTreeVal 方法查找目標節點,見代碼塊4詳解。
7.校驗節點數是否超過 8 個,如果超過則調用 treeifyBin方法 將鏈表節點轉爲紅黑樹節點,見代碼塊6詳解。
代碼塊4:putTreeVal
/**
* 紅黑樹的put操作,紅黑樹插入會同時維護原來的鏈表屬性, 即原來的next屬性
*/
final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
// 1.查找根節點, 索引位置的頭節點並不一定爲紅黑樹的根節點
TreeNode<K,V> root = (parent != null) ? root() : this;
// 2.將根節點賦值給p節點,開始進行查找
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
// 3.如果傳入的hash值小於p節點的hash值,將dir賦值爲-1,代表向p的左邊查找樹
if ((ph = p.hash) > h)
dir = -1;
// 4.如果傳入的hash值大於p節點的hash值, 將dir賦值爲1,代表向p的右邊查找樹
else if (ph < h)
dir = 1;
// 5.如果傳入的hash值和key值等於p節點的hash值和key值, 則p節點即爲目標節點, 返回p節點
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
// 6.如果k所屬的類沒有實現Comparable接口 或者 k和p節點的key相等
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
// 6.1 第一次符合條件, 從p節點的左節點和右節點分別調用find方法進行查找, 如果查找到目標節點則返回
if (!searched) {
TreeNode<K,V> q, ch;
searched = true;
if (((ch = p.left) != null &&
(q = ch.find(h, k, kc)) != null) ||
((ch = p.right) != null &&
(q = ch.find(h, k, kc)) != null))
return q;
}
// 6.2 否則使用定義的一套規則來比較k和p節點的key的大小, 用來決定向左還是向右查找
dir = tieBreakOrder(k, pk); // dir<0則代表k<pk,則向p左邊查找;反之亦然
}
TreeNode<K,V> xp = p; // xp賦值爲x的父節點,中間變量,用於下面給x的父節點賦值
// 7.dir<=0則向p左邊查找,否則向p右邊查找,如果爲null,則代表該位置即爲x的目標位置
if ((p = (dir <= 0) ? p.left : p.right) == null) {
// 走進來代表已經找到x的位置,只需將x放到該位置即可
Node<K,V> xpn = xp.next; // xp的next節點
// 8.創建新的節點, 其中x的next節點爲xpn, 即將x節點插入xp與xpn之間
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
// 9.調整x、xp、xpn之間的屬性關係
if (dir <= 0) // 如果時dir <= 0, 則代表x節點爲xp的左節點
xp.left = x;
else // 如果時dir> 0, 則代表x節點爲xp的右節點
xp.right = x;
xp.next = x; // 將xp的next節點設置爲x
x.parent = x.prev = xp; // 將x的parent和prev節點設置爲xp
// 如果xpn不爲空,則將xpn的prev節點設置爲x節點,與上文的x節點的next節點對應
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
// 10.進行紅黑樹的插入平衡調整
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
6.1 第一次符合條件,從 p 節點的左節點和右節點分別調用 find 方法(見代碼塊2詳解)進行查找,如果查找到目標節點則返回
6.2 否則使用定義的一套規則來比較 k 和 p 節點的 key 的大小,用來決定向左還是向右查找,見代碼塊5詳解。
10.進行紅黑樹的插入平衡調整,見文末的解釋2。
代碼塊5:tieBreakOrder
// 用於不可比較或者hashCode相同時進行比較的方法, 只是一個一致的插入規則,用來維護重定位的等價性。
static int tieBreakOrder(Object a, Object b) {
int d;
if (a == null || b == null ||
(d = a.getClass().getName().
compareTo(b.getClass().getName())) == 0)
d = (System.identityHashCode(a) <= System.identityHashCode(b) ?
-1 : 1);
return d;
}
定義一套規則用於極端情況下比較兩個參數的大小。
代碼塊6:treeifyBin
/**
* 將鏈表節點轉爲紅黑樹節點
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
// 1.如果table爲空或者table的長度小於64, 調用resize方法進行擴容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
// 2.根據hash值計算索引值,將該索引位置的節點賦值給e,從e開始遍歷該索引位置的鏈表
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
// 3.將鏈表節點轉紅黑樹節點
TreeNode<K,V> p = replacementTreeNode(e, null);
// 4.如果是第一次遍歷,將頭節點賦值給hd
if (tl == null) // tl爲空代表爲第一次循環
hd = p;
else {
// 5.如果不是第一次遍歷,則處理當前節點的prev屬性和上一個節點的next屬性
p.prev = tl; // 當前節點的prev屬性設爲上一個節點
tl.next = p; // 上一個節點的next屬性設置爲當前節點
}
// 6.將p節點賦值給tl,用於在下一次循環中作爲上一個節點進行一些鏈表的關聯操作(p.prev = tl 和 tl.next = p)
tl = p;
} while ((e = e.next) != null);
// 7.將table該索引位置賦值爲新轉的TreeNode的頭節點,如果該節點不爲空,則以以頭節點(hd)爲根節點, 構建紅黑樹
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
7.將 table 該索引位置賦值爲新轉的 TreeNode 的頭節點 hd,如果該節點不爲空,則以 hd 爲根節點,構建紅黑樹,見代碼塊7詳解。
代碼塊7:treeify
/**
* 構建紅黑樹
*/
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
// 1.將調用此方法的節點賦值給x,以x作爲起點,開始進行遍歷
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next; // next賦值爲x的下個節點
x.left = x.right = null; // 將x的左右節點設置爲空
// 2.如果還沒有根節點, 則將x設置爲根節點
if (root == null) {
x.parent = null; // 根節點沒有父節點
x.red = false; // 根節點必須爲黑色
root = x; // 將x設置爲根節點
}
else {
K k = x.key; // k賦值爲x的key
int h = x.hash; // h賦值爲x的hash值
Class<?> kc = null;
// 3.如果當前節點x不是根節點, 則從根節點開始查找屬於該節點的位置
for (TreeNode<K,V> p = root;;) {
int dir, ph;
K pk = p.key;
// 4.如果x節點的hash值小於p節點的hash值,則將dir賦值爲-1, 代表向p的左邊查找
if ((ph = p.hash) > h)
dir = -1;
// 5.如果x節點的hash值大於p節點的hash值,則將dir賦值爲1, 代表向p的右邊查找
else if (ph < h)
dir = 1;
// 6.走到這代表x的hash值和p的hash值相等,則比較key值
else if ((kc == null && // 6.1 如果k沒有實現Comparable接口 或者 x節點的key和p節點的key相等
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
// 6.2 使用定義的一套規則來比較x節點和p節點的大小,用來決定向左還是向右查找
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p; // xp賦值爲x的父節點,中間變量用於下面給x的父節點賦值
// 7.dir<=0則向p左邊查找,否則向p右邊查找,如果爲null,則代表該位置即爲x的目標位置
if ((p = (dir <= 0) ? p.left : p.right) == null) {
// 8.x和xp節點的屬性設置
x.parent = xp; // x的父節點即爲最後一次遍歷的p節點
if (dir <= 0) // 如果時dir <= 0, 則代表x節點爲父節點的左節點
xp.left = x;
else // 如果時dir > 0, 則代表x節點爲父節點的右節點
xp.right = x;
// 9.進行紅黑樹的插入平衡(通過左旋、右旋和改變節點顏色來保證當前樹符合紅黑樹的要求)
root = balanceInsertion(root, x);
break;
}
}
}
}
// 10.如果root節點不在table索引位置的頭節點, 則將其調整爲頭節點
moveRootToFront(tab, root);
}
3.如果當前節點 x 不是根節點, 則從根節點開始查找屬於該節點的位置,該段代碼跟代碼塊2和代碼塊4的查找代碼類似。
8.如果 root 節點不在 table 索引位置的頭節點, 則將其調整爲頭節點,見代碼塊8詳解。
代碼塊8:moveRootToFront
/**
* 將root放到頭節點的位置
* 如果當前索引位置的頭節點不是root節點, 則將root的上一個節點和下一個節點進行關聯,
* 將root放到頭節點的位置, 原頭節點放在root的next節點上
*/
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
int n;
// 1.校驗root是否爲空、table是否爲空、table的length是否大於0
if (root != null && tab != null && (n = tab.length) > 0) {
// 2.計算root節點的索引位置
int index = (n - 1) & root.hash;
TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
// 3.如果該索引位置的頭節點不是root節點,則該索引位置的頭節點替換爲root節點
if (root != first) {
Node<K,V> rn;
// 3.1 將該索引位置的頭節點賦值爲root節點
tab[index] = root;
TreeNode<K,V> rp = root.prev; // root節點的上一個節點
// 3.2 和 3.3 兩個操作是移除root節點的過程
// 3.2 如果root節點的next節點不爲空,則將root節點的next節點的prev屬性設置爲root節點的prev節點
if ((rn = root.next) != null)
((TreeNode<K,V>)rn).prev = rp;
// 3.3 如果root節點的prev節點不爲空,則將root節點的prev節點的next屬性設置爲root節點的next節點
if (rp != null)
rp.next = rn;
// 3.4 和 3.5 兩個操作將first節點接到root節點後面
// 3.4 如果原頭節點不爲空, 則將原頭節點的prev屬性設置爲root節點
if (first != null)
first.prev = root;
// 3.5 將root節點的next屬性設置爲原頭節點
root.next = first;
// 3.6 root此時已經被放到該位置的頭節點位置,因此將prev屬性設爲空
root.prev = null;
}
// 4.檢查樹是否正常
assert checkInvariants(root);
}
}
4.檢查樹是否正常,見代碼塊9詳解。
代碼塊9:checkInvariants
/**
* Recursive invariant check
*/
static <K,V> boolean checkInvariants(TreeNode<K,V> t) { // 一些基本的校驗
TreeNode<K,V> tp = t.parent, tl = t.left, tr = t.right,
tb = t.prev, tn = (TreeNode<K,V>)t.next;
if (tb != null && tb.next != t)
return false;
if (tn != null && tn.prev != t)
return false;
if (tp != null && t != tp.left && t != tp.right)
return false;
if (tl != null && (tl.parent != t || tl.hash > t.hash))
return false;
if (tr != null && (tr.parent != t || tr.hash < t.hash))
return false;
if (t.red && tl != null && tl.red && tr != null && tr.red) // 如果當前節點爲紅色, 則該節點的左右節點都不能爲紅色
return false;
if (tl != null && !checkInvariants(tl))
return false;
if (tr != null && !checkInvariants(tr))
return false;
return true;
}
將傳入的節點作爲根節點,遍歷所有節點,校驗節點的合法性,主要是保證該樹符合紅黑樹的規則。
resize 方法
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
// 1.老表的容量不爲0,即老表不爲空
if (oldCap > 0) {
// 1.1 判斷老表的容量是否超過最大容量值:如果超過則將閾值設置爲Integer.MAX_VALUE,並直接返回老表,
// 此時oldCap * 2比Integer.MAX_VALUE大,因此無法進行重新分佈,只是單純的將閾值擴容到最大
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 1.2 將newCap賦值爲oldCap的2倍,如果newCap<最大容量並且oldCap>=16, 則將新閾值設置爲原來的兩倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 2.如果老表的容量爲0, 老表的閾值大於0, 是因爲初始容量被放入閾值,則將新表的容量設置爲老表的閾值
else if (oldThr > 0)
newCap = oldThr;
else {
// 3.老表的容量爲0, 老表的閾值爲0,這種情況是沒有傳初始容量的new方法創建的空表,將閾值和容量設置爲默認值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 4.如果新表的閾值爲空, 則通過新的容量*負載因子獲得閾值
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 5.將當前閾值設置爲剛計算出來的新的閾值,定義新表,容量爲剛計算出來的新容量,將table設置爲新定義的表。
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 6.如果老表不爲空,則需遍歷所有節點,將節點賦值給新表
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) { // 將索引值爲j的老表頭節點賦值給e
oldTab[j] = null; // 將老表的節點設置爲空, 以便垃圾收集器回收空間
// 7.如果e.next爲空, 則代表老表的該位置只有1個節點,計算新表的索引位置, 直接將該節點放在該位置
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// 8.如果是紅黑樹節點,則進行紅黑樹的重hash分佈(跟鏈表的hash分佈基本相同)
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
// 9.如果是普通的鏈表節點,則進行普通的重hash分佈
Node<K,V> loHead = null, loTail = null; // 存儲索引位置爲:“原索引位置”的節點
Node<K,V> hiHead = null, hiTail = null; // 存儲索引位置爲:“原索引位置+oldCap”的節點
Node<K,V> next;
do {
next = e.next;
// 9.1 如果e的hash值與老表的容量進行與運算爲0,則擴容後的索引位置跟老表的索引位置一樣
if ((e.hash & oldCap) == 0) {
if (loTail == null) // 如果loTail爲空, 代表該節點爲第一個節點
loHead = e; // 則將loHead賦值爲第一個節點
else
loTail.next = e; // 否則將節點添加在loTail後面
loTail = e; // 並將loTail賦值爲新增的節點
}
// 9.2 如果e的hash值與老表的容量進行與運算爲1,則擴容後的索引位置爲:老表的索引位置+oldCap
else {
if (hiTail == null) // 如果hiTail爲空, 代表該節點爲第一個節點
hiHead = e; // 則將hiHead賦值爲第一個節點
else
hiTail.next = e; // 否則將節點添加在hiTail後面
hiTail = e; // 並將hiTail賦值爲新增的節點
}
} while ((e = next) != null);
// 10.如果loTail不爲空(說明老表的數據有分佈到新表上“原索引位置”的節點),則將最後一個節點
// 的next設爲空,並將新表上索引位置爲“原索引位置”的節點設置爲對應的頭節點
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 11.如果hiTail不爲空(說明老表的數據有分佈到新表上“原索引+oldCap位置”的節點),則將最後
// 一個節點的next設爲空,並將新表上索引位置爲“原索引+oldCap”的節點設置爲對應的頭節點
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
// 12.返回新表
return newTab;
}
2.老表的容量爲 0,並且老表的閾值大於 0:這種情況是新建 HashMap 時傳了初始容量,例如:new HashMap<>(32),使用這種方式新建 HashMap 時,由於 HashMap 沒有 capacity 屬性,所以此時的 capacity 會被暫存在 threshold 屬性。因此此時的 threshold 的值就是我們要新創建的 HashMap 的 capacity,所以將新表的容量設置爲 threshold。
4.如果新表的閾值爲空,則通過新的容量 * 負載因子獲得閾值(這種情況是初始化的時候傳了初始容量,跟第2點相同情況,或者初始容量設置的太小導致老表的容量沒有超過 16 導致的)。
8.如果是紅黑樹節點,則進行紅黑樹的重 hash 分佈,見代碼塊10詳解。
9.1 如果 e 的 hash 值與老表的容量進行位與運算爲 0,則說明 e 節點擴容後的索引位置跟老表的索引位置一樣(見例子1詳解),進行鏈表拼接操作:如果 loTail 爲空,代表該節點爲第一個節點,則將 loHead 賦值爲該節點;否則將節點添加在 loTail 後面,並將 loTail 賦值爲新增的節點。
9.2 如果 e 的 hash 值與老表的容量進行位與運算爲 1,則說明 e 節點擴容後的索引位置爲:老表的索引位置+oldCap(見例子1詳解),進行鏈表拼接操作:如果 hiTail 爲空,代表該節點爲第一個節點,則將 hiHead 賦值爲該節點;否則將節點添加在 hiTail 後面,並將 hiTail 賦值爲新增的節點。
代碼塊10:split
/**
* 擴容後,紅黑樹的hash分佈,只可能存在於兩個位置:原索引位置、原索引位置+oldCap
*/
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this; // 拿到調用此方法的節點
TreeNode<K,V> loHead = null, loTail = null; // 存儲索引位置爲:“原索引位置”的節點
TreeNode<K,V> hiHead = null, hiTail = null; // 存儲索引位置爲:“原索引+oldCap”的節點
int lc = 0, hc = 0;
// 1.以調用此方法的節點開始,遍歷整個紅黑樹節點
for (TreeNode<K,V> e = b, next; e != null; e = next) { // 從b節點開始遍歷
next = (TreeNode<K,V>)e.next; // next賦值爲e的下個節點
e.next = null; // 同時將老表的節點設置爲空,以便垃圾收集器回收
// 2.如果e的hash值與老表的容量進行與運算爲0,則擴容後的索引位置跟老表的索引位置一樣
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null) // 如果loTail爲空, 代表該節點爲第一個節點
loHead = e; // 則將loHead賦值爲第一個節點
else
loTail.next = e; // 否則將節點添加在loTail後面
loTail = e; // 並將loTail賦值爲新增的節點
++lc; // 統計原索引位置的節點個數
}
// 3.如果e的hash值與老表的容量進行與運算爲1,則擴容後的索引位置爲:老表的索引位置+oldCap
else {
if ((e.prev = hiTail) == null) // 如果hiHead爲空, 代表該節點爲第一個節點
hiHead = e; // 則將hiHead賦值爲第一個節點
else
hiTail.next = e; // 否則將節點添加在hiTail後面
hiTail = e; // 並將hiTail賦值爲新增的節點
++hc; // 統計索引位置爲原索引+oldCap的節點個數
}
}
// 4.如果原索引位置的節點不爲空
if (loHead != null) { // 原索引位置的節點不爲空
// 4.1 如果節點個數<=6個則將紅黑樹轉爲鏈表結構
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
// 4.2 將原索引位置的節點設置爲對應的頭節點
tab[index] = loHead;
// 4.3 如果hiHead不爲空,則代表原來的紅黑樹(老表的紅黑樹由於節點被分到兩個位置)
// 已經被改變, 需要重新構建新的紅黑樹
if (hiHead != null)
// 4.4 以loHead爲根節點, 構建新的紅黑樹
loHead.treeify(tab);
}
}
// 5.如果索引位置爲原索引+oldCap的節點不爲空
if (hiHead != null) { // 索引位置爲原索引+oldCap的節點不爲空
// 5.1 如果節點個數<=6個則將紅黑樹轉爲鏈表結構
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
// 5.2 將索引位置爲原索引+oldCap的節點設置爲對應的頭節點
tab[index + bit] = hiHead;
// 5.3 loHead不爲空則代表原來的紅黑樹(老表的紅黑樹由於節點被分到兩個位置)
// 已經被改變, 需要重新構建新的紅黑樹
if (loHead != null)
// 5.4 以hiHead爲根節點, 構建新的紅黑樹
hiHead.treeify(tab);
}
}
}
2.如果 e 的 hash 值與老表的容量進行位與運算爲 0,則說明 e 節點擴容後的索引位置跟老表的索引位置一樣(見例子1詳解),進行鏈表拼接操作:如果 loTail 爲空,代表該節點爲第一個節點,則將 loHead 賦值爲該節點;否則將節點添加在 loTail 後面,並將 loTail 賦值爲新增的節點,並統計原索引位置的節點個數。
3.如果 e 的 hash 值與老表的容量進行位與運算爲 1,則說明 e 節點擴容後的索引位置爲:老表的索引位置+oldCap(見例子1詳解),進行鏈表拼接操作:如果 hiTail 爲空,代表該節點爲第一個節點,則將 hiHead 賦值爲該節點;否則將節點添加在 hiTail 後面,並將 hiTail 賦值爲新增的節點,並統計索引位置爲原索引 + oldCap 的節點個數。
4.1 如果節點個數 <= 6 個則將紅黑樹轉爲鏈表結構,見代碼塊11詳解。
4.4 以 loHead 爲根節點,構建新的紅黑樹,見代碼塊7詳解。
代碼塊11:untreeify
/**
* 將紅黑樹節點轉爲鏈表節點, 當節點<=6個時會被觸發
*/
final Node<K,V> untreeify(HashMap<K,V> map) {
Node<K,V> hd = null, tl = null; // hd指向頭節點, tl指向尾節點
// 1.從調用該方法的節點, 即鏈表的頭節點開始遍歷, 將所有節點全轉爲鏈表節點
for (Node<K,V> q = this; q != null; q = q.next) {
// 2.調用replacementNode方法構建鏈表節點
Node<K,V> p = map.replacementNode(q, null);
// 3.如果tl爲null, 則代表當前節點爲第一個節點, 將hd賦值爲該節點
if (tl == null)
hd = p;
// 4.否則, 將尾節點的next屬性設置爲當前節點p
else
tl.next = p;
tl = p; // 5.每次都將tl節點指向當前節點, 即尾節點
}
// 6.返回轉換後的鏈表的頭節點
return hd;
}
例子1:擴容後,節點重 hash 爲什麼只可能分佈在 “原索引位置” 與 “原索引 + oldCap 位置” ?
擴容代碼中,使用 e 節點的 hash 值跟 oldCap 進行位與運算,以此決定將節點分佈到 “原索引位置” 或者 “原索引 + oldCap 位置” 上,這是爲什麼了?
假設老表的容量爲 16,即 oldCap = 16,則新表容量爲 16 * 2 = 32,假設節點 1 的 hash 值爲:0000 0000 0000 0000 0000 1111 0000 1010,節點 2 的 hash 值爲:0000 0000 0000 0000 0000 1111 0001 1010,則節點 1 和節點 2 在老表的索引位置計算如下圖計算1,由於老表的長度限制,節點 1 和節點 2 的索引位置只取決於節點 hash 值的最後 4 位。
再看計算2,計算2爲新表的索引計算,可以知道如果兩個節點在老表的索引位置相同,則新表的索引位置只取決於節點hash值倒數第5位的值,而此位置的值剛好爲老表的容量值 16,此時節點在新表的索引位置只有兩種情況:“原索引位置” 和 “原索引 + oldCap位置”,在此例中即爲 10 和 10 + 16 = 26。
由於結果只取決於節點 hash 值的倒數第 5 位,而此位置的值剛好爲老表的容量值 16,因此此時新表的索引位置的計算可以替換爲計算3,直接使用節點的 hash 值與老表的容量 16 進行位於運算,如果結果爲 0 則該節點在新表的索引位置爲原索引位置,否則該節點在新表的索引位置爲 “原索引 + oldCap 位置”。
remove 方法
/**
* 移除某個節點
*/
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
// 1.如果table不爲空並且根據hash值計算出來的索引位置不爲空, 將該位置的節點賦值給p
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
// 2.如果p的hash值和key都與入參的相同, 則p即爲目標節點, 賦值給node
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
// 3.否則將p.next賦值給e,向下遍歷節點
// 3.1 如果p是TreeNode則調用紅黑樹的方法查找節點
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
// 3.2 否則,進行普通鏈表節點的查找
do {
// 當節點的hash值和key與傳入的相同,則該節點即爲目標節點
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e; // 賦值給node, 並跳出循環
break;
}
p = e; // p節點賦值爲本次結束的e,在下一次循環中,e爲p的next節點
} while ((e = e.next) != null); // e指向下一個節點
}
}
// 4.如果node不爲空(即根據傳入key和hash值查找到目標節點),則進行移除操作
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
// 4.1 如果是TreeNode則調用紅黑樹的移除方法
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
// 4.2 如果node是該索引位置的頭節點則直接將該索引位置的值賦值爲node的next節點,
// “node == p”只會出現在node是頭節點的時候,如果node不是頭節點,則node爲p的next節點
else if (node == p)
tab[index] = node.next;
// 4.3 否則將node的上一個節點的next屬性設置爲node的next節點,
// 即將node節點移除, 將node的上下節點進行關聯(鏈表的移除)
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node); // 供LinkedHashMap使用
// 5.返回被移除的節點
return node;
}
}
return null;
}
3.1 如果 p 是 TreeNode 則調用紅黑樹的方法查找節點,見代碼塊1詳解。
4.1 如果是 TreeNode 則調用紅黑樹的移除方法,見代碼塊12詳解。
代碼塊12:removeTreeNode
這塊代碼比較長,目的就是移除調用此方法的節點,也就是該方法中的 this 節點。移除包括鏈表的處理和紅黑樹的處理。可以結合下文的圖解理解。
/**
* 紅黑樹的節點移除
*/
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
boolean movable) {
// --- 鏈表的處理start ---
int n;
// 1.table爲空或者length爲0直接返回
if (tab == null || (n = tab.length) == 0)
return;
// 2.根據hash計算出索引的位置
int index = (n - 1) & hash;
// 3.將索引位置的頭節點賦值給first和root
TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
// 4.該方法被將要被移除的node(TreeNode)調用, 因此此方法的this爲要被移除node節點,
// 將node的next節點賦值給succ節點,prev節點賦值給pred節點
TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
// 5.如果pred節點爲空,則代表要被移除的node節點爲頭節點,
// 則將table索引位置的值和first節點的值賦值爲succ節點(node的next節點)即可
if (pred == null)
tab[index] = first = succ;
else
// 6.否則將pred節點的next屬性設置爲succ節點(node的next節點)
pred.next = succ;
// 7.如果succ節點不爲空,則將succ的prev節點設置爲pred, 與前面對應
if (succ != null)
succ.prev = pred;
// 8.如果進行到此first節點爲空,則代表該索引位置已經沒有節點則直接返回
if (first == null)
return;
// 9.如果root的父節點不爲空, 則將root賦值爲根節點
if (root.parent != null)
root = root.root();
// 10.通過root節點來判斷此紅黑樹是否太小, 如果是則調用untreeify方法轉爲鏈表節點並返回
// (轉鏈表後就無需再進行下面的紅黑樹處理)
if (root == null || root.right == null ||
(rl = root.left) == null || rl.left == null) {
tab[index] = first.untreeify(map); // too small
return;
}
// --- 鏈表的處理end ---
// --- 以下代碼爲紅黑樹的處理 ---
// 11.將p賦值爲要被移除的node節點,pl賦值爲p的左節點,pr賦值爲p 的右節點
TreeNode<K,V> p = this, pl = left, pr = right, replacement;
// 12.如果p的左節點和右節點都不爲空時
if (pl != null && pr != null) {
// 12.1 將s節點賦值爲p的右節點
TreeNode<K,V> s = pr, sl;
// 12.2 向左一直查找,跳出循環時,s爲沒有左節點的節點
while ((sl = s.left) != null)
s = sl;
// 12.3 交換p節點和s節點的顏色
boolean c = s.red; s.red = p.red; p.red = c;
TreeNode<K,V> sr = s.right; // s的右節點
TreeNode<K,V> pp = p.parent; // p的父節點
// --- 第一次調整和第二次調整:將p節點和s節點進行了位置調換 ---
// 12.4 第一次調整
// 如果p節點的右節點即爲s節點,則將p的父節點賦值爲s,將s的右節點賦值爲p
if (s == pr) {
p.parent = s;
s.right = p;
}
else {
// 將sp賦值爲s的父節點
TreeNode<K,V> sp = s.parent;
// 將p的父節點賦值爲sp
if ((p.parent = sp) != null) {
// 如果s節點爲sp的左節點,則將sp的左節點賦值爲p節點
if (s == sp.left)
sp.left = p;
// 否則s節點爲sp的右節點,則將sp的右節點賦值爲p節點
else
sp.right = p;
}
// s的右節點賦值爲p節點的右節點
if ((s.right = pr) != null)
// 如果pr不爲空,則將pr的父節點賦值爲s
pr.parent = s;
}
// 12.5 第二次調整
// 將p的左節點賦值爲空,pl已經保存了該節點
p.left = null;
// 將p節點的右節點賦值爲sr,如果sr不爲空,則將sr的父節點賦值爲p節點
if ((p.right = sr) != null)
sr.parent = p;
// 將s節點的左節點賦值爲pl,如果pl不爲空,則將pl的父節點賦值爲s節點
if ((s.left = pl) != null)
pl.parent = s;
// 將s的父節點賦值爲p的父節點pp
// 如果pp爲空,則p節點爲root節點, 交換後s成爲新的root節點
if ((s.parent = pp) == null)
root = s;
// 如果p不爲root節點, 並且p是pp的左節點,則將pp的左節點賦值爲s節點
else if (p == pp.left)
pp.left = s;
// 如果p不爲root節點, 並且p是pp的右節點,則將pp的右節點賦值爲s節點
else
pp.right = s;
// 12.6 尋找replacement節點,用來替換掉p節點
// 12.6.1 如果sr不爲空,則replacement節點爲sr,因爲s沒有左節點,所以使用s的右節點來替換p的位置
if (sr != null)
replacement = sr;
// 12.6.1 如果sr爲空,則s爲葉子節點,replacement爲p本身,只需要將p節點直接去除即可
else
replacement = p;
}
// 13.承接12點的判斷,如果p的左節點不爲空,右節點爲空,replacement節點爲p的左節點
else if (pl != null)
replacement = pl;
// 14.如果p的右節點不爲空,左節點爲空,replacement節點爲p的右節點
else if (pr != null)
replacement = pr;
// 15.如果p的左右節點都爲空, 即p爲葉子節點, replacement節點爲p節點本身
else
replacement = p;
// 16.第三次調整:使用replacement節點替換掉p節點的位置,將p節點移除
if (replacement != p) { // 如果p節點不是葉子節點
// 16.1 將p節點的父節點賦值給replacement節點的父節點, 同時賦值給pp節點
TreeNode<K,V> pp = replacement.parent = p.parent;
// 16.2 如果p沒有父節點, 即p爲root節點,則將root節點賦值爲replacement節點即可
if (pp == null)
root = replacement;
// 16.3 如果p不是root節點, 並且p爲pp的左節點,則將pp的左節點賦值爲替換節點replacement
else if (p == pp.left)
pp.left = replacement;
// 16.4 如果p不是root節點, 並且p爲pp的右節點,則將pp的右節點賦值爲替換節點replacement
else
pp.right = replacement;
// 16.5 p節點的位置已經被完整的替換爲replacement, 將p節點清空, 以便垃圾收集器回收
p.left = p.right = p.parent = null;
}
// 17.如果p節點不爲紅色則進行紅黑樹刪除平衡調整
// (如果刪除的節點是紅色則不會破壞紅黑樹的平衡無需調整)
TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);
// 18.如果p節點爲葉子節點, 則簡單的將p節點去除即可
if (replacement == p) {
TreeNode<K,V> pp = p.parent;
// 18.1 將p的parent屬性設置爲空
p.parent = null;
if (pp != null) {
// 18.2 如果p節點爲父節點的左節點,則將父節點的左節點賦值爲空
if (p == pp.left)
pp.left = null;
// 18.3 如果p節點爲父節點的右節點, 則將父節點的右節點賦值爲空
else if (p == pp.right)
pp.right = null;
}
}
if (movable)
// 19.將root節點移到索引位置的頭節點
moveRootToFront(tab, r);
}
7.如果 succ 節點不爲空,則將 succ 的 prev 節點設置爲 pred,與前面對應(TreeNode 鏈表的移除,見開頭第8點)。
12.6 尋找 replacement,用來替換掉 p 節點。爲什麼 sr 是 replacement 的首選,p 爲備選?見解釋1。
PS:代碼中的第一次調整和第二次調整是將 p 節點和 s 節點進行了位置調換,然後找出要替換掉 p 節點的 replacement;第三次調整是將 replacement 節點覆蓋掉 p 節點;這部分的代碼邏輯非常複雜,建議自己動手畫圖模擬。(下文圖解1即爲這三次調整的例子)
解釋1:爲什麼 sr 是 replacement 的首選,p 爲備選?
解析:首先我們看 sr 是什麼?從代碼中可以看到 sr 第一次被賦值時,是在 s 節點進行了向左窮遍歷結束後,因此此時 s 節點是沒有左節點的,sr 即爲 s 節點的右節點。而從上面的第一次調整和第二次調整我們知道,p 節點已經跟 s 節點進行了位置調換,所以此時 sr 其實是 p 節點的右節點,並且 p 節點沒有左節點,因此要移除 p 節點,只需要將 p 節點的右節點 sr 覆蓋掉 p 節點即可,因此 sr 是 replacement 的首選,而如果 sr 爲空,則代表 p 節點爲葉子節點,此時將 p 節點直接移除即可。
圖解1:removeTreeNode 圖解
本圖解忽略紅黑樹的顏色,請注意。
下面的圖解是代碼中的最複雜的情況,即流程最長的那個,p 節點不爲根節點,p 節點有左右節點,s 節點不爲 pr 節點,s 節點有右節點。
另外,第一次調整和第二次調整的是本人根據代碼而設定的,將第一次調整和第二次調整合起來看會更容易理解,如下:
第一次調整 + 第二次調整:將 p 節點和 s 節點進行了位置調換,選出要替換掉 p 節點的 replacement
第三次調整:將 replacement 節點覆蓋掉 p 節點
解釋2:關於紅黑樹的平衡調整?
答:紅黑樹的操作涉及的操作比較複雜,三言兩語無法說清。有興趣的可以去單獨學習,本文由於篇幅關係暫不詳細介紹紅黑樹的具體操作,在這簡單的介紹:紅黑樹是一種自平衡二叉樹,擁有優秀的查詢和插入/刪除性能,廣泛應用於關聯數組。
對比 AVL 樹,AVL 要求每個節點的左右子樹的高度之差的絕對值(平衡因子)最多爲 1,而紅黑樹通過適當的放低該條件(紅黑樹限制從根到葉子的最長的可能路徑不多於最短的可能路徑的兩倍長,結果是這個樹大致上是平衡的),以此來減少插入/刪除時的平衡調整耗時,從而獲取更好的性能,而這雖然會導致紅黑樹的查詢會比 AVL 稍慢,但相比插入/刪除時獲取的時間,這個付出在大多數情況下顯然是值得的。
在 HashMap 中的應用:HashMap 在進行插入和刪除時有可能會觸發紅黑樹的插入平衡調整(balanceInsertion 方法)或刪除平衡調整(balanceDeletion 方法),調整的方式主要有以下手段:左旋轉(rotateLeft 方法)、右旋轉(rotateRight 方法)、改變節點顏色(x.red = false、x.red = true),進行調整的原因是爲了維持紅黑樹的數據結構。
死循環問題
在 JDK 1.8 以前,Java 語言在併發情況下使用 HashMap 造成 Race Condition,從而導致死循環。程序經常佔了 100% 的 CPU,查看堆棧,你會發現程序都 Hang 在了 “HashMap.get()” 這個方法上了,重啓程序後問題消失。具體分析可以查看這篇文章:疫苗:JAVA HASHMAP的死循環,有人將這個問題當成一個 bug 提給了 Sun,但是 Sun 認爲這並不是個 bug,因爲HashMap 本來就不保證併發的線程安全性,在併發下,要用 ConcurrentHashMap 來代替。
那麼,在JDK 1.8 的時候,這個問題解決了嗎?
我們知道,JDK 1.8 以前,導致死循環的主要原因是擴容後,節點的順序會反掉,如下圖:擴容前節點 A 在節點 C 前面,而擴容後節點 C 在節點 A 前面。
JDK 1.8擴容過程
JDK1.8 普通鏈表的擴容代碼,如下圖所示,在上文已經分析過了:主要是在一個 do/while 中處理同一個位置的所有節點。
舉個例子
前提:我們假設有3個節點,節點 A,節點 B,節點 C,並且假設他們的 hash 值等於 key 值,則按上圖擴容的過程模擬如下。
先看下老表和新表計算索引位置的過程:(hash 計算省略前面 28 位 0,只看最後 4 位)
具體擴容過程:
結果:可以看出,擴容後,節點 A 和節點 C 的先後順序跟擴容前是一樣的。因此,即使此時有多個線程併發擴容,也不會出現死循環的情況。當然,這仍然改變不了 HashMap 仍是非併發安全,在併發下,還是要使用 ConcurrentHashMap 來代替。
HashMap 和 Hashtable 的區別
HashMap 允許 key 和 value 爲 null,Hashtable 不允許。
HashMap 的默認初始容量爲 16,Hashtable 爲 11。
HashMap 的擴容爲原來的 2 倍,Hashtable 的擴容爲原來的 2 倍加 1。
HashMap 是非線程安全的,Hashtable是線程安全的。
HashMap 的 hash 值重新計算過,Hashtable 直接使用 hashCode。
HashMap 去掉了 Hashtable 中的 contains 方法。
HashMap 繼承自 AbstractMap 類,Hashtable 繼承自 Dictionary 類。
總結
HashMap 的底層是個 Node 數組(Node<K,V>[] table),在數組的具體索引位置,如果存在多個節點,則可能是以鏈表或紅黑樹的形式存在。
增加、刪除、查找鍵值對時,定位到哈希桶數組的位置是很關鍵的一步,源碼中是通過下面3個操作來完成這一步:1)拿到 key 的 hashCode 值;2)將 hashCode 的高位參與運算,重新計算 hash 值;3)將計算出來的 hash 值與 “table.length - 1” 進行 & 運算。
HashMap 的默認初始容量(capacity)是 16,capacity 必須爲 2 的冪次方;默認負載因子(load factor)是 0.75;實際能存放的節點個數(threshold,即觸發擴容的閾值)= capacity * load factor。
HashMap 在觸發擴容後,閾值會變爲原來的 2 倍,並且會對所有節點進行重 hash 分佈,重 hash 分佈後節點的新分佈位置只可能有兩個:“原索引位置” 或 “原索引+oldCap位置”。例如 capacity 爲16,索引位置 5 的節點擴容後,只可能分佈在新表 “索引位置5” 和 “索引位置21(5+16)”。
導致 HashMap 擴容後,同一個索引位置的節點重 hash 最多分佈在兩個位置的根本原因是:1)table的長度始終爲 2 的 n 次方;2)索引位置的計算方法爲 “(table.length - 1) & hash”。HashMap 擴容是一個比較耗時的操作,定義 HashMap 時儘量給個接近的初始容量值。
HashMap 有 threshold 屬性和 loadFactor 屬性,但是沒有 capacity 屬性。初始化時,如果傳了初始化容量值,該值是存在 threshold 變量,並且 Node 數組是在第一次 put 時纔會進行初始化,初始化時會將此時的 threshold 值作爲新表的 capacity 值,然後用 capacity 和 loadFactor 計算新表的真正 threshold 值。
當同一個索引位置的節點在增加後達到 9 個時,並且此時數組的長度大於等於 64,則會觸發鏈表節點(Node)轉紅黑樹節點(TreeNode),轉成紅黑樹節點後,其實鏈表的結構還存在,通過 next 屬性維持。鏈表節點轉紅黑樹節點的具體方法爲源碼中的 treeifyBin 方法。而如果數組長度小於64,則不會觸發鏈表轉紅黑樹,而是會進行擴容。
當同一個索引位置的節點在移除後達到 6 個時,並且該索引位置的節點爲紅黑樹節點,會觸發紅黑樹節點轉鏈表節點。紅黑樹節點轉鏈表節點的具體方法爲源碼中的 untreeify 方法。
HashMap 在 JDK 1.8 之後不再有死循環的問題,JDK 1.8 之前存在死循環的根本原因是在擴容後同一索引位置的節點順序會反掉。
HashMap 是非線程安全的,在併發場景下使用 ConcurrentHashMap 來代替。
————————————————
版權聲明:本文爲CSDN博主「程序員囧輝」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/v123411739/article/details/78996181