概述
HashMap 作爲平時開發過程中常見的數據結構經常被用到,網上關於它的博文已經有非常多,爲了加深我對它的理解,我計劃就 JDK1.8 版本,通過源碼的形式整理下它的原理。
HashMap
HashMap在源碼中是這樣定義的:
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable,Serializable
這裏我通過簡單類圖描述一下這幾個類之間的關係:
-
Map 接口主要聲明一些常用的 key-value 接口方法
-
AbstractMap 抽象類實現 Map 接口,它實現了部分接口方法,以及創建靜態內部類 SimpleEntry。
-
Cloneable 接口主要和 clone() 方法有關,實現該接口可以重寫 clone() 方法
-
Serializable 主要和序列化有關,實現該接口可以讓對象序列換
關於 SimpleEntry 類這裏我們先不詳細說明,等後面用到了我們再說
構造方法
HashMap 類的構造方法主要有以下幾種:
static final float DEFAULT_LOAD_FACTOR = 0.75f;
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
其中前三種構造方法比較常見,第四種構造方法用的比較少,這裏我們主要以常用的前三種爲主。
通過源碼我們可以看出,前三種構造方法主要是爲了初始化屬性 loadFactor 和 threshold 的值。這裏我直接給出結論:
-
loadFactor:hashMap 的負載因子,當集合中元素個數達到負載因子所對應數量後 hashMap 會 擴容,通過源碼可以看出默認的負載因子是0.75
-
threshold:hashMap 所對應管道數,每個管道可以保存多個元素
下面我們看一下 threshold 屬性是如何計算出來的,即 tableSizeFor() 方法的源碼:
static final int MAXIMUM_CAPACITY = 1 << 30;
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
通過該方法可以獲取大於等於參數的最小的2的冪次方:也就是說,如果參數是5,輸出8,參數是9,輸出16,每次都輸出大於當前參數的最小二次冪。
從這裏也就可以看出,hashMap 的管道個數總是2的冪次方,關於這樣做的原因後面根據源碼具體分析。下面我們看一個草圖,通過這個草圖對 hashMap 有一個大致的認識:
put() 方法
有了上面的鋪墊,下面我們具體看一下 hashmap 是如何保存一個元素的:
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){
// 暫時先省略,下面着重解決
}
這裏我先給出 putVal() 方法這五個參數所代表的意義:
- hash:key 的 hash 值
- key :要保存的 key
- value:要保存的 value
- onlyIfAbsent:是否修改已經存在的數據,如果該參數爲 true,則不會修改已經存在的key
- evict:該參數在 hashMap 中沒有用到,在 hashMap 的子類 linkedHashMap 有用到,如果該參數爲true,在添加元素後可能會刪除頂部元素。
接下來我們看一個 hash() 方法的實現:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
該方法只是將 key 的 hashCode() 值散列的高位向地位移動一下。
Node內部類
在正式開始閱讀 putVal() 源碼前,我們先看看內部類 Node 的源碼,該類是 putVal() 方法的基礎:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
// 省略 get() set()
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
通過源碼我們可以看出,Node 內部保存 key、value 、hash 值以及指向下一個節點的 next 引用。其中它重寫了 hashCode() 方法 和 equals() 方法。關於爲啥重寫這兩個方法可以點擊這裏參考我之前的博客。
其實看到這裏 hashMap 的原理基本上已經可以猜出來了,只需要把上面的草圖改成下面這樣:
也就是說,hashMap 實際上就是通過 Node 內部類組成的數組和鏈表實現的
putVal() 源碼
有了上面這些基礎,我們具體來看 putVal() 方法具體是怎麼做的。在下面源碼中我儘量通過註釋的形式介紹,部分特別重要的內容附加在源碼後面:
// transient 表示該屬性不會序列化,這裏table就表示上圖中的數組塊內容
transient Node<K,V>[] table;
// 單個鏈表的閾值
static final int TREEIFY_THRESHOLD = 8;
// 數組中工作節點個數
transient int modCount;
// 當前hashMap存儲的節點個數
transient int size;
// hashMap 所能容納的最大節點個數
int threshold;
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 說明數組還沒有初始化,即 hashmap 第一次添加元素時
if ((tab = table) == null || (n = tab.length) == 0)
// resize()方法初始化數組,關於該方法的源碼下面我會給出,這裏的n表示數組的長度,可以理解爲hashMap的管道數
n = (tab = resize()).length;
// 判斷管道頭是否爲空,如果爲空,直接將要put的元素添加到數組對應下標
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 判斷數組元素key是否等於要put的元素key
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 判斷是否使用TreeNode
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
// 從鏈表頭依次判斷是否存在節點key與參數相等
for (int binCount = 0; ; ++binCount) {
// 已經遍歷到鏈表尾部
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 如果某個鏈表的節點個數達到閾值
if (binCount >= TREEIFY_THRESHOLD - 1)
//
treeifyBin(tab, hash);
break;
}
// 判斷是否相等
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) {
// 這裏的 e可能是新添加的Node節點,也可能是老的Node節點(hash相等,key相等情況)
V oldValue = e.value;
// 更新新值
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 該方法在hashMap爲空,什麼都沒做
afterNodeAccess(e);
// 返回給節點對應的老值
return oldValue;
}
}
// 工作節點個數加一
++modCount;
// 判斷hashmap元素數是否大於閾值
if (++size > threshold)
// 擴容
resize();
// 該方法在 hashmap 爲空
afterNodeInsertion(evict);
return null;
}
上述代碼中,我主要提一下這行代碼:
p = tab[(n - 1) & hash]
從這裏我們可以看出,元素屬於哪個數組節點是由它的hash值和數組長度決定的。元素的hash值是隨機的,數組的長度是確定的,爲了讓元素儘可能平均的分配到所有節點,(n-1) & hash 的計算結果必須儘可能的均勻。
當元素的長度總是2的冪次方時,n-1的值轉化爲二進制總是111…1。在這種情況下,隨機的hash值計算出的結果也就相對比較均勻,這也是爲什麼 hashmap 中數組的長度總是2的冪次方的主要原因。
-
爲什麼要讓元素分配的相對均勻呢?
元素在數組上分配均勻無論是添加還是修改或是查詢,都只需要遍歷較少的節點數量,這對於效率的提升至關重要。
hashmap 的初始化及擴容
在整理 putval() 方法時,我們提到 hashmap 是在第一次put元素時初始化,當元素數量超過閾值時,擴容也是調用該方法,下面我整理一下 resize() 方法的源碼:
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
// 原先的數組大小
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 原先hashmap所能容納的最大元素數
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
// 數組已經達到最大值
if (oldCap >= MAXIMUM_CAPACITY) {
// 將最大元素數設置爲 MAX
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 數組長度擴容爲原來的兩倍,如果hashmap最大容量超過16,也變爲原來的兩倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1;
}
// 如果數組長度爲空,最大元素數不爲0,將數組長度設置爲容量大小
else if (oldThr > 0)
newCap = oldThr;
// hashmap還未初始化時,此時容量以及數組大小都爲0
else {
// 默認數組大小爲16
newCap = DEFAULT_INITIAL_CAPACITY;
// 默認hashmap閾值爲 16 * 0.75(負載因子)
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 如果此時hashmap容量爲0
if (newThr == 0) {
// 重新計算最大容量
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 如果是數組擴容,重新分配所有元素
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
// 數組節點只有一個元素時的情況
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// 存的是TreeNode時的情況
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
// 循環重新計算所有節點屬於哪個下標
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
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;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
上述代碼中,我主要提一下 hashmap 擴容時元素重新分配的原理:
-
如果某個數組節點對應的鏈表只有一個元素,直接根據新的數組長度計算
-
如果某個數組節點對應的鏈表有多個元素,則會根據一個 奇妙的算法計算
首先,hashmap 每次擴容時,數組長度都會變爲原來的 2倍。轉化爲二進制就可以這樣表示:
而計算元素屬於哪個數組下標是和數組長度減1來計算的,數組長度減1轉二進制分別對應:
- 原來的數組長度轉二進制:N 個 “1”
- 現在的數組長度轉二進制:N + 1 個 “1”
也就是說,通過新數組長度所計算出的下標,其實就是多算了一位第 n+1 位的 “1”,舉個例子:
hash值等於10的元素轉二進制 -> 1010,將它保存到長度爲2的數組
此時屬於哪個下標 -> 1010 & 0001,也就是說只看最後一位
如果數組長度擴容爲原來的二倍
此時屬於哪個小標 -> 1010 & 0011,也就是說,在原來的基礎上,往前多判斷一位
寫成公式:擴容後的下標 = 擴容前下標 + 新加位是否爲1
爲了判斷這個新加位是否爲1,只需要讓hash值直接和原數組長度進行&運算即可,
也就是上面代碼所對應的 e.hash & oldCap
這個新加長度所對應的值,實際上也就是 oldCap
總結一下,數組擴容後,當前元素要麼屬於原來的數組下標,要麼屬於原來的數組下標加上原數組長度
Node 轉 TreeNode
在整理 putVal() 方法時,當單個鏈表元素數量超過鏈表閾值時會調用 treeifyBin() 方法,在正式學習treeifyBin() 方法源碼前,我們先看看內部類 TreeNode 的結構:
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;
// 省略部分實現好的內部方法
}
從該對象的屬性很明顯可以看出,它是一種樹形結果,並且是性能較好的 紅黑樹。而 LinkedHashMap.Entry 又是 Node 的子類,也就是說,該TreeNode 也是 Node 的子類,通過這種方式實現了 Node 轉 TreeNode 的可能性,因爲對象可以 向上轉型。
下面我們具體看 treeifyBin() 方法的源碼:
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)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
// Node 轉 TreeNode
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
該方法是在某個鏈表超過閾值時,將所有Node節點轉換爲TreeNode節點,並通過前驅、後綴引用連接起來,下面我們主要看一下 treeify() 方法的源碼:
final void treeify(Node<K,V>[] tab) {
// 創建頭節點
TreeNode<K,V> root = null;
// 循環當前鏈表節點
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
// 初始化頭結點
if (root == null) {
x.parent = null;
// 表明當前節點是“黑節點”
x.red = false;
root = x;
}
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
// 根據 hash 值向左向右遍歷
for (TreeNode<K,V> p = root;;) {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
// 如果hash值相等,根據comparable接口方法判斷是否相等
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
// 記錄當前節點,表示要判斷節點的父節點
TreeNode<K,V> xp = p;
// 只有進入該方法,才表示找的真正的落點,進行保存
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp;
if (dir <= 0)
xp.left = x;
else
xp.right = x;
// 重新讓樹表的平衡
root = balanceInsertion(root, x);
break;
}
}
}
}
// 確定數組對應節點是紅黑樹的頭節點
moveRootToFront(tab, root);
}
該方法表示當某個鏈表長度超過閾值時,將鏈表轉換爲紅黑樹的結構,提高效率。關於紅黑樹的結構我們後面做專門介紹(內容實在太多),就不再這裏深入方法本身做探討了。
get() 方法
看過put()方法的源碼, 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;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
看過 put() 方法源碼後,看 get() 方法簡直不要太輕鬆,幾乎沒有什麼新內容,這裏我們簡單描述一下如果鏈表結構爲 TreeNode 紅黑樹時,如何遍歷。具體我們看代碼:
final TreeNode<K,V> getTreeNode(int h, Object k) {
// 這裏主要保證每次遍歷總是從最高級節點開始,root() 方法只是循環遍歷到父節點
return ((parent != null) ? root() : this).find(h, k, null);
}
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
TreeNode<K,V> p = this;
do {
int ph, dir; K pk;
TreeNode<K,V> pl = p.left, pr = p.right, q;
// 向左遍歷
if ((ph = p.hash) > h)
p = pl;
// 向右遍歷
else if (ph < h)
p = pr;
// hash 相等且 key 相等,說明就是當前節點
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
// hash 相等,但是左節點爲空時,向右遍歷
else if (pl == null)
p = pr;
// hash 相等,但是右節點爲空時,向左遍歷
else if (pr == null)
p = pl;
// kc爲 true 時,根據 compare 方法判斷,默認爲false
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
p = (dir < 0) ? pl : pr;
// 根據左節點查詢
else if ((q = pr.find(h, k, kc)) != null)
return q;
// 根據右節點查詢
else
p = pl;
} while (p != null);
return null;
}
總結一下:當鏈表轉換爲紅黑樹結構後,通過hash值作爲參考,key值作爲一錘定音的判斷來實現的。當hash值相等時,根據對象的 compare() 方法做判斷
putTreeVal() 方法
有了上面紅黑樹的基礎,我們再來看看,當鏈表結構轉化爲紅黑樹結構後,如何添加新元素。具體我們看源碼:
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;
// 定位到最頂級父節點
TreeNode<K,V> root = (parent != null) ? root() : this;
for (TreeNode<K,V> p = root;;) {
int dir, ph; K pk;
if ((ph = p.hash) > h)
dir = -1;
else if (ph < h)
dir = 1;
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
// compare() 方法相同只會遍歷一次
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;
}
// 通過 compareTo 方法和 identityHashCode() 確定方向
dir = tieBreakOrder(k, pk);
}
TreeNode<K,V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node<K,V> xpn = xp.next;
TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
if (dir <= 0)
xp.left = x;
else
xp.right = x;
xp.next = x;
x.parent = x.prev = xp;
if (xpn != null)
((TreeNode<K,V>)xpn).prev = x;
// 將重新平衡後的紅黑樹的頭結點設置在數組中
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
從源碼可以看出,當鏈表轉換爲紅黑樹後,添加元素的邏輯實際上和轉化時大致相同。
紅黑樹的擴容
上面我們講了鏈表的擴容方法,這裏我們主要看看當鏈表轉換爲紅黑樹後,整個紅黑樹的擴容方法,即 split() 方法的源碼:
final void split(HashMap<K,V> map, Node<K,V>[] tab, int index, int bit) {
TreeNode<K,V> b = this;
// Relink into lo and hi lists, preserving order
TreeNode<K,V> loHead = null, loTail = null;
TreeNode<K,V> hiHead = null, hiTail = null;
int lc = 0, hc = 0;
// 遍歷整個紅黑樹,將整個紅黑樹改爲兩個鏈表,一個鏈表記錄新增位 & 爲0,表示擴容後還在當前下標,另一個記錄新增位 & 爲1,表示擴容後再原下標+老數組長度下標
for (TreeNode<K,V> e = b, next; e != null; e = next) {
next = (TreeNode<K,V>)e.next;
e.next = null;
if ((e.hash & bit) == 0) {
if ((e.prev = loTail) == null)
loHead = e;
else
loTail.next = e;
loTail = e;
++lc;
}
else {
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}
// 處理對應數組下標不會變的情況
if (loHead != null) {
// 如果長度小於6,TreeNode 轉爲 Node
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);
}
}
}
總結以下:紅黑樹的擴容就是遍歷整個紅黑樹,將結果轉爲兩個鏈表,一個鏈表記錄下標會變的情況,另一個鏈表記錄下標不會變的情況,根據鏈表的長度,決定是否將鏈表轉Node鏈表,還是紅黑樹化。
紅黑樹的平衡
在鏈表處理轉紅黑樹或給紅黑樹添加新元素後,都會執行 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;;) {
// 表示 x 就是root節點,直接返回
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
// 如果x的父節點就是黑色,並且它沒有父節點,直接返回root,表示只有兩個節點,root就是根節點
else if (!xp.red || (xpp = xp.parent) == null)
return root;
// 如果x的父節點是它自身父節點的左子樹
if (xp == (xppl = xpp.left)) {
// x 的父節點的父節點的右子樹不爲空
if ((xppr = xpp.right) != null && xppr.red) {
xppr.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
// x 的父節點的父節點的右子樹爲空
else {
// 判斷 x 是否它父節點的右子樹
if (x == xp.right) {
// 向左旋轉x的父節點
root = rotateLeft(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) {
xp.red = false;
if (xpp != null) {
xpp.red = true;
// 向右旋轉
root = rotateRight(root, xpp);
}
}
}
}
// 如果是右子樹,下述情況和上面對稱
else {
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);
}
}
}
}
}
}
上述代碼本身比較複雜,主要涉及紅黑樹的左旋、右旋以及顏色的處理。關於這塊我暫時也看的迷迷糊糊,等後續整理紅黑樹時着重整理,我暫時先給出兩個關於左旋以及右旋的示意圖:
如上圖所示,就可以讓樹變得相對比較平衡。關於紅黑樹的只是暫且整理到這裏,後面加上顏色着重分析。
關於 hashmap 的源碼我就整理到這裏,對於一些其他方法的實現,我想如果你讀懂了 put() 以及 get() 方法,那基本不會有啥難度。