HashMap之鏈表轉紅黑樹(樹化 )-treefyBin方法源碼解讀(所有涉及到的方法均有詳細解讀,歡迎指正)

PS:由於文檔是我在本地編寫好之後再複製過來的,有些文本格式沒能完整的體現,故提供下述圖片,供大家閱覽,以便有更好的閱讀體驗:
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

HashMap之鏈表轉紅黑樹(樹化 )-treefyBin方法
方法概述:先將鏈表節點轉爲樹節點,再將都是紅黑樹節點的鏈表轉爲紅黑樹
分析HashMap的put方法的源碼時發現,當HashMap中某個鏈表上存儲的元素個數達到TREEIFY_THRESHOLD(樹化閾值)=8個時,會調用treeifyBin方法嘗試將該鏈表轉換成紅黑樹。
(PS:爲什麼說是嘗試,而不是直接轉呢?因爲,在做轉換之前,還會對HashMap的鏈表數組存儲的元素個數進行判斷,當HashMap存儲的總元素個數達到MIN_TREEIFY_CAPACITY(樹化最小容量,HashMap存儲的元素總個數)=64時,纔會進行鏈表到紅黑樹的轉換;否則,只進行擴容。)
概括起來,將鏈表轉爲紅黑樹主要有以下處理:
 判斷鏈表中存儲的元素個數是否到達樹化閾值8,沒達到不往下處理
 判斷HashMap中鏈表數組的長度是否達到樹化最小容量64,沒達到則擴容,而非轉紅黑樹
 將需要樹化的鏈表中的所有節點替換成樹節點。
 將所有節點已經是紅黑樹節點的鏈表轉爲紅黑樹
具體,詳見下述的源碼解析:

1. HashMap的put(K,V)方法中調用treeifyBin()的源碼

方法概述:鏈表長度達到樹化閾值時,開始調用treeifyBin(tab,hash)嘗試轉紅黑樹
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st//當鏈表中存儲的元素個數達到(等於)樹化閾值=8時,開始鏈表轉紅黑樹的處理
treeifyBin(tab, hash); //開始嘗試將鏈表結構轉成紅黑樹結構,但還保留有鏈表的順序結構,具體方法源碼詳見第2點
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}

2. HashMap的treeifyBin(tab,hash)方法的源碼

方法概述:HashMap數組長度達到樹化最小容量,開始樹化操作:先將鏈表中的節點都替換成樹節點,再將樹節點組成的鏈表轉爲紅黑樹
Replaces all linked nodes in bin at index for given hash unless table is too small, in which case resizes instead.將給定hash索引的桶中的所有鏈表節點替換掉,除非數組太小的話,就用擴容方法替
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)//當HashMap存儲鏈表頭節點的數組爲null或其長度達不到(小於等於)樹化最小容量=64時,則進行擴容而非樹化操作。
resize();//調用擴容方法
else if ((e = tab[index = (n - 1) & hash]) != null) {//hash指定的數組索引位置的頭節點不爲空才繼續
TreeNode<K,V> hd = null, tl = null; //hd代表處理過程中需要被轉成紅黑樹的鏈表的頭節head,tl代表處理過程中需要被轉爲紅黑樹節點的鏈表的實時最新當前尾節點tail
do {
TreeNode<K,V> p = replacementTreeNode(e, null); //將鏈表類型Node<K,V>的節點轉成樹類型TreeNode<K,V>的節點,具體方法源碼詳見第3點。
if (tl == null) //tl尾節點爲null,說明還沒開始第一次處理(注:每次有一個鏈表頭節點走進do{}while{}循環體,tl只會在第一次的時候爲null,因爲後續會將非null的p=replacementTreeNode(e(非null),null)賦給了tl,這樣可以保證只有鏈表頭節點纔會賦給hd)
hd = p; //把頭節點賦給hd
else {
p.prev = tl; //將當前要處理的非頭節點的上一節點指向實時最新當前尾節點tl
tl.next = p; //將實時最新當前尾節點tl的下一節點指向當前要處理的非頭節點
}
tl = p;//將當前已經處理完的非頭節點置爲最新的尾節點tl
} while ((e = e.next) != null); //有後續節點時繼續循環,否則退出循環
if ((tab[index] = hd) != null) //處理後的hd頭節點不爲空,纔有料,才需轉紅黑樹
hd.treeify(tab); //將已經全部被替換爲樹類型節點的鏈表(頭節點hd可以代表鏈表,因爲通過鏈表頭節點的next指向可以遍歷整個鏈表)轉爲紅黑樹,具體方法源碼詳見第4點
}
}

3. HashMap的treeifyBin()方法中調用replacementTreeNode(e,null)的源碼

方法概述:將鏈表節點轉爲樹節點
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
return new TreeNode<>(p.hash, p.key, p.value, next); //爲樹化操作做前期準備:將鏈表類型Node<K,V>每個節點都轉成樹類型TreeNode<K,V>節點(注:如果再深入一些,會發現一個很有意思的事情,原來HashMap中的TreeNode<K,V> extends LinkedHashMap.Entry<K,V> extends HashMap.Node<K,V>,構造方法層層往上調用又回到了HashMap的Node<K,V>,說明TreeNode既是樹類型結構,同時也是鏈表類型結構)
}
/**HashMapKaTeX parse error: Expected '}', got 'EOF' at end of input: …點和下一節點(從HashMapNode繼承而來),所以是雙向鏈表
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
/…紅黑樹的其他相關代碼省略…/
}
/*LinkedHashMapEntry<K,V>//HashMap.NodesubclassfornormalLinkedHashMapentries./staticclassEntry<K,V>extendsHashMap.Node<K,V>Entry<K,V>before,after;Entry(inthash,Kkey,Vvalue,Node<K,V>next)super(hash,key,value,next);/HashMapEntry<K,V>的構造方法源碼*/ /*** HashMap.Node subclass for normal LinkedHashMap entries. */ static class Entry<K,V> extends HashMap.Node<K,V> { Entry<K,V> before, after; Entry(int hash, K key, V value, Node<K,V> next) { super(hash, key, value, next); } } /**HashMapNode<K,V>的構造方法源碼/
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next; //由於HashMap的Node只存了下一節點,所以是單向鏈表
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
/…鏈表的其他相關代碼省略…/
}

4. HashMap%TreeNode的treeify(tab)方法源碼

方法概述:將都是樹節點的鏈表轉爲紅黑樹,這裏纔是真正的樹化操作
Forms tree of the nodes linked from this node
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;//定義樹根節點
for (TreeNode<K,V> x = this, next; x != null; x = next) {//x=hd手樹鏈表頭節點
next = (TreeNode<K,V>)x.next; //下一節點,保證在鏈表中循環遞進
x.left = x.right = null; //紅黑樹左右分支節點先初始化爲null
if (root == null) {//root只會等於一次null,後續不會再進到這裏
x.parent = null; //根節點無父節點
x.red = false; //根節點非紅,即黑
root = x; //將頭節點賦給根節點
}
else {//處理完頭節點/根節點後,處理其他後續節點
K k = x.key; //k代表鍵key
int h = x.hash; //h代表hash值
Class<?> kc = null; //kc代表鍵對應的類型
for (TreeNode<K,V> p = root; ; ) {//目前循環只定義了從root根節點出發
int dir, ph; //dir決定節點屬於左子樹節點還是右子樹節點;ph代表p節點的hash碼
K pk = p.key; //pk代表p節點的key
if ((ph = p.hash) > h) //根據hash碼大小比較,決定向左還是向右
dir = -1; //更大的向右走
else if (ph < h)
dir = 1; //更小的向左走
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||//該方法用於判斷鍵k所屬類型是否屬於Comparable接口的實現類型,是則返回具體類型,不是則返回null;具體方法源碼詳見第5點
(dir = compareComparables(kc, k, pk)) == 0) //該方法用於返回k和pk的比較結果,如果x和k的類型kc都是可比較類型,則直接返回k和pk的比較結果,否則返回0,具體方法源碼詳見第6點
dir = tieBreakOrder(k, pk); //該方法用於返回k和pk的決勝局最終PK結果,具體方法源碼詳見第7點
TreeNode<K,V> xp = p; //存留p的值,用於後續處理
if ((p = (dir <= 0) ? p.left : p.right) == null) {//根據dir判斷元素應該放入左分支還是右分支,左右分支若已存在元素,則繼續循環遞進前進進行k比較 (從p=root根節點到p.left左分支或p.right右分支再到下一級的左分支或右分支)
x.parent = xp; //x的父節點是xp即p(x肯定是root後面的節點,因爲if(root=null)裏root=x處理完後,x會走到next=x.next,往下走)(x.next和p.left/p.right將鏈表和紅黑樹串起來了)
if (dir <= 0) //小於等於0屬於左節點
xp.left = x; //將鏈表節點轉爲紅黑樹左節點
else
xp.right = x; //大於0屬於右節點, 將鏈表節點轉爲紅黑樹右節點
root = balanceInsertion(root, x); //該方法用於確保插入元素後的紅黑樹實現自平衡(因爲新插入元素後可能會打破紅黑樹原有的平衡(五大原則)),具體方法源碼詳見第8點
break;
}
}
}
}
moveRootToFront(tab, root); //該方法用於確保根節點始終是頭節點,具體方法源碼詳見第9點
}

5. HashMap%TreeNode的comparableClassFor(k)方法源碼

方法概述:該方法用於判斷鍵k所屬類型是否屬於Comparable(可比較)接口的實現類型,是則返回具體類型,不是則返回null
Returns x’s Class if it is of the form “class C implements Comparable”, else null.
//判斷x的類型是否屬於Comparable接口的實現類,不是則返回null
static Class<?> comparableClassFor(Object x) {
if (x instanceof Comparable) {
Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
//c用於存x的類型;ts用於存c實現的所有父接口;p用於存儲ts的參數化接口類型
if ((c = x.getClass()) == String.class) // bypass checks //判斷x的類型是否屬於String類型
return c;//由於String類型是Comparable接口的實現類,所以直接返回
if ((ts = c.getGenericInterfaces()) != null) {//取出c的所有父接口信息並存到ts中
for (int i = 0; i < ts.length; ++i) {
if (((t = ts[i]) instanceof ParameterizedType) &&//某一父接口屬於參數化接口
((p = (ParameterizedType)t).getRawType() ==Comparable.class) &&//並且該參數化類型接口就是Comparable接口
(as = p.getActualTypeArguments()) != null && as.length == 1 && as[0] == c) // type arg is c //並且該參數化接口的參數類型數組不爲空,且其長度爲1,說明數組中只有唯一一個類型,就是x的類型c
return c; //返回x的類型c
}
}
}
return null; //否則返回null,說明x的類型不屬於Comparable接口的實現類
}

6. HashMap%TreeNode的compareComparables(kc, k, pk)方法源碼

方法概述:該方法用於返回k和pk的比較結果,如果x和k的類型kc都是可比較類型,則直接返回k和pk的比較結果,否則返回0
Returns k.compareTo(x) if x matches kc (k’s screened comparable class), else 0.
//如果x和k都是可比較類型,則返回k和x的比較結果,否則返回0

@SuppressWarnings({"rawtypes","unchecked"}) // for cast to Comparable
static int compareComparables(Class<?> kc, Object k, Object x) {
    return (x == null || x.getClass() != kc ? 0 : //(結合第5點看,走到這裏說明k的類型kc是可比較類型)若x等於null或者x的類型不等於kc,說明x和k不具有可比性,此時會返回0
            ((Comparable)k).compareTo(x)); //走到這裏說明x不等於null並且x的l和k是同一類型,即均爲可比較的類型,開始用compareTo方法進行比較,返回-1/0/1.

}

7. HashMap%TreeNode的tieBreakOrder(k, pk)方法源碼

方法概述:該方法用於返回k和pk的決勝局最終PK結果
Tie-breaking utility for ordering insertions when equal hashCodes and non-comparable.
//決勝局功能是爲了在hashCode相等且沒法比較時,用於決定如何順序插入
We don’t require a total order, just a consistent insertion rule to maintain equivalence across
//我們不需要全序,只需通過再平衡,用一個始終如一保持一致的插入規則來維持平衡
rebalancings. Tie-breaking further than necessary simplifies testing a bit.
/決勝局進一步簡化了測試。/
static int tieBreakOrder(Object a, Object b) {
int d; //初始值爲0
if ( a == null || b == null ||//當a或b兩者有一個爲null(即沒法直接相互比較)
(d = a.getClass().getName().compareTo(b.getClass().getName())) == 0) //或者兩者類型相同
d = (System.identityHashCode(a) <= System.identityHashCode(b) ? -1 : 1//開始調用System.identityHashCode對a和b兩者間進行終極PK,小於等於返回-1,大於返回1,並將結果賦給d
);
return d; //返回最終PK結果(可能是-1/0/1)
}

8. HashMap%TreeNode的balanceInsertion(root, x)方法源碼

方法概述:保證在插入元素後紅黑樹仍能保持其自平衡,發現衝突則開始再平衡調節
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root, TreeNode<K,V> x) {
x.red = true;
for (TreeNode<K,V> xp, xpp, xppl, xppr;😉 {
if ((xp = x.parent) == null) {//父節點爲null,說明x爲根節點
x.red = false; //根節點是黑色
return x; //返回是根節點的x
}
else if (!xp.red || (xpp = xp.parent) == null) //x的父節點xp爲黑色(與x是紅黑不衝突)或x的祖父節點xpp爲null(xpp是根節點,即root),這兩種情況不會引發紅黑樹不平衡,故直接返回root根節點
return root;
//後續xp是紅色(衝突來啦!x紅且xp紅,不滿足紅黑樹自平衡規則3),因爲xp是黑色的話,在上面的else if裏就返回了,程序不會再繼續往下進行了
//xpp非null,原因同上
//下面開始分情況處理:
if (xp == (xppl = xpp.left)) {//x的父節點xp是x的祖父節點xpp的左子節點xppl
if ((xppr = xpp.right) != null && xppr.red) {//x的伯父節點不爲空且爲紅色:開始變色,將父節點和伯父節點變爲黑色,將祖父節點變爲紅色即可
xppr.red = false; //伯父節點變爲黑色
xp.red = false; //父節點變爲黑色
xpp.red = true; //祖父節點變爲紅色
x = xpp; //繼續下一輪循環
}
else {//走到這裏說明x的伯父節點爲空或是黑色
if (x == xp.right) {//x是其父節點xp的右孩子節點(x和xp均紅,開始左旋調整)
root = rotateLeft(root, x = xp); //左旋xp節點(x和xp是兩個連續的紅色節點,x爲xp的右子節點,則開始左旋).注意:左旋後x和xp的父子關係互換,具體方法源碼詳見第10點
xpp = (xp = x.parent) == null ? null : xp.parent; //左旋後xp和xpp需要根據跟原xp互換父子關係後的x重新定義賦值,左旋後x變成了xp的父節點,xp爲x的子節點,所以這裏的x.parent實際其實指的是原xp現在的父節點x,但是在業務邏輯上,現在的x應該被定義爲xp.故xp=x.parent,則xpp=新xp.parent.
}
if (xp != null) { //左旋後的新的xp若不爲空
xp.red = false; //將xp置爲黑色,這樣x和xp就不再是連續的兩個紅色節點了
if (xpp != null) { //若xpp不爲空
xpp.red = true; //統一將xpp設置爲紅色,配合下面的右旋保持一致操作
root = rotateRight(root, xpp); //右旋xpp節點(可能會導致xpp和其父節點變成兩個連續的紅色節點,由於xp爲xpp的左子節點,所以嘗試開始右旋),具體方法源碼詳見第11點
}
}
}
}
else {//x的父節點xp是x的祖父節點xpp的右子節點xppr,處理邏輯同上,不再贅述…
if (xppl != null && xppl.red) {
xppl.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
if (x == xp.left) {
root = rotateRight(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateLeft(root, xpp);
}
}
}
}
}
}

9. HashMap%TreeNode的moveRootToFront (tab, root)方法源碼

方法概述:該方法用於確保根節點始終是頭節點
/*** Ensures that the given root is the first node of its bin.*/
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
int n;
if (root != null && tab != null && (n = tab.length) > 0) {
int index = (n - 1) & root.hash;
TreeNode<K,V> first = (TreeNode<K,V>)tab[index]; //目前的頭節點
if (root != first) {//根節點不等於目前的頭節點
Node<K,V> rn;
tab[index] = root; //將根節點設置爲頭節點
TreeNode<K,V> rp = root.prev;
if ((rn = root.next) != null)
((TreeNode<K,V>)rn).prev = rp; //將根節點的下一節點的上一節點指向根節點的上一節點
if (rp != null)
rp.next = rn; //將根節點的上一節點的下一節點指向根節點的下一節點
if (first != null)
first.prev = root; //將原頭節點的上一節點指向根節點
root.next = first; //將根節點的下一節點指向原頭節點
root.prev = null; //將根節點的上一節點設爲null
}
assert checkInvariants(root); //檢查根節點是否滿足紅黑樹節點規則, 具體方法源碼詳見第12點
}
}

10. HashMap%TreeNode的rotateLeft(root, x = xp)方法源碼

方法概述:該方法用於左旋調整紅黑樹(以p節點爲中心左旋) ,會發生父子關係互換
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root, TreeNode<K,V> p) {
TreeNode<K,V> r, pp, rl;
if (p != null && (r = p.right) != null) { //p不爲空,且p的右孩子節點不爲空
if ((rl = p.right = r.left) != null) //將p的右孩子節點r的左孩子節點rl設爲p的右孩子節點
rl.parent = p; //將p的右孩子節點r的左孩子節點rl的父節點設爲p
if ((pp = r.parent = p.parent) == null)//將p的右孩子的父節點設爲p的父節點
(root = r).red = false; //若p爲根節點,左旋用p的右孩子節點替換p後,則將p的右孩子節點設爲黑色
else if (pp.left == p) //若p的父節點不爲空,且p爲其父節點pp的左孩子節點
pp.left = r; //則將p父節點的左孩子節點替換成p節點原來的右孩子節點r
else//若p的父節點不爲空,且p爲其父節點pp的右孩子節點
pp.right = r; //則將p父節點的右孩子節點替換成p節點原來的右孩子節點r
r.left = p; //p成爲其原右孩子節點的左孩子節點
p.parent = r; //p的父節點爲其原右孩子節點
}
return root;
}
示意圖:
在這裏插入圖片描述

總結:左旋後,p節點成爲了其右子節點的左子節點;其右子節點的左節點成了p的右子節點。
相當於r用rl跟p換了個位置

11. HashMap%TreeNode的rotateRight(root, p)方法源碼

方法概述:該方法用於右旋調整紅黑樹(以p節點爲中心右旋) ,會發生父子關係互換
static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root, TreeNode<K,V> p) {
TreeNode<K,V> l, pp, lr;
if (p != null && (l = p.left) != null) { //p不爲空,且p的左孩子節點不爲空
if ((lr = p.left = l.right) != null) //將p的左孩子節點l的右孩子節點lr設爲p的左孩子節點
lr.parent = p; //將p的左孩子節點l的右孩子節點lr的父節點設爲p
if ((pp = l.parent = p.parent) == null) //將p的左孩子的父節點設爲p的父節點
(root = l).red = false; //若p爲根節點,右旋用p的左孩子節點替換p後,則將p的左孩子節點設爲黑色
else if (pp.right == p) //若p的父節點不爲空,且p爲其父節點pp的右孩子節點
pp.right = l; //則將p父節點的右孩子節點替換成p節點原來的左孩子節點l
else //若p的父節點不爲空,且p爲其父節點pp的左孩子節點
pp.left = l; //則將p父節點的左孩子節點替換成p節點原來的左孩子節點l
l.right = p; //p成爲其原左孩子節點的右孩子節點
p.parent = l; //p的父節點爲其原左孩子節點
}
return root;
}
示意圖:
在這裏插入圖片描述
總結:右旋後,p節點成爲了其左子節點的右子節點;其左子節點的右節點成了p的左子節點。相當於l用lr跟p換了個位置。

12. HashMap%TreeNode的checkInvariants (root)方法源碼

方法概述:該方法用於校驗插入或刪除後的鏈表/紅黑樹是否正常
/*** 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) //t.prev的下一個節點不是t
return false;
if (tn != null && tn.prev != t) //t.next的上一個節點不是t
return false;
if (tp != null && t != tp.left && t != tp.right) //t既不是t.parent的左子節點也不是右子節點
return false;
if (tl != null && (tl.parent != t || tl.hash > t.hash)) //t的左子節點的父節點不是t,或t的左子節點的hash值大於t的hash值
return false;
if (tr != null && (tr.parent != t || tr.hash < t.hash)) //t的右子節點的父節點不是t,或t的右子節點的hash值小於t的hash值
return false;
if (t.red && tl != null && tl.red && tr != null && tr.red) //t爲紅色節點且t的左右子節點均爲紅(父節點和其中的一個子節點爲紅,還可以通過左旋右旋調整,兩個都爲紅色說明肯定有問題)
return false;
if (tl != null && !checkInvariants(tl)) //對t的左子節點tl繼續上述檢查
return false;
if (tr != null && !checkInvariants(tr)) //對t的右子節點tr繼續上述檢查
return false;
return true; //上述檢查都沒問題,才返回true
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章