要思考的問題
- HashMap的底層數據結構(節點結構,這種結構有什麼優點)
- 如何處理hash衝突
- 怎麼擴容?擴展機制是什麼?
- 增刪改查過程
- 鏈表到紅黑樹的轉換過程,反之?
- 紅黑樹相關(見另一篇數據結構之紅黑樹)
- hash計算
達到的目標
- 掌握底層數據結構
- 掌握擴容原理
- 掌握hash衝突的處理過程
- 掌握增刪改查過程
看之前要掌握的知識點
紅黑樹
看之前大體瞭解的知識點
hash算法
Poisson分佈
開始
HashMap的繼承體系
[外鏈圖片轉存失敗(img-z4jA6cu0-1567747065188)(./images/HashMap01-繼承體系.png)]
- AbstractMap: map的抽象類,以最大限度的減少實現Map接口的類的工作量。
hashMap結構
字段解釋
常量字段(默認值字段)
- DEFAULT_INITIAL_CAPACITY=1<<4: 默認的初始容量,默認是爲16,必須是2的n次方.爲什麼呢? 見擴容的方法。
- DEFAULT_LOAD_FACTOR=0.75f: 默認的負載因子。它和哈希表的容量的乘積是決定是否重新hash的閾值。
- TREEIFY_THRESHOLD=8: 使用樹而不是鏈表的計數閾值。當桶的元素添加到具有至少這麼多節點時,桶被轉換爲樹。
- UNTREEIFY_THRESHOLD=6: 用於在調整大小操作期間解除(拆分)桶的桶計數閾值。(untreeifying不是一個英語單詞,這裏的以是非樹化,即轉換成普通列表的過程).也就是說從樹轉換成普通的桶(鏈表)的閾值。
- MAXIMUM_CAPACITY=1<<30: 最大的容量:
1<<30
,如果具有參數的任一構造函數隱式指定更高的值,則使用此參數。必須是2的n次方,小於等於1<<30
- MIN_TREEIFY_CAPACITY=64: 容器可以樹化的最小容量(否則,如果bin中的節點太多,則會調整表的大小.)應該至少爲 4 * TREEIFY_THRESHOLD,以避免調整大小和樹化閾值之間的衝突.
類屬性
- table:
transient HashMap.Node<K,V>[] table
; table在首次使用時初始化,並根據需要調整大小。分配時,長度始終是2的冪。(我們還在一些操作中容忍長度爲零,以允許當前不需要的自舉機制) - entrySet:
transient Set<Map.Entry<K,V>> entrySet
; 保存緩存的entrySet. - size:
transient int size
; map中元素的數量。結構修改是那些改變HashMap中映射數量或以其他方式修改其內部結構(例如,rehash)的修改。此字段用於在HashMap的Collection-views上快速生成迭代器(見ConcurrentModificationException)
注意: 這些字段都是 transient
的? 爲什麼呢?
- loadFactor:
final float loadFactor;
hash表的負載因子,在實例化hashTable的時候指定,該對象內不能變更(final); - threshold:
int threshold;
, 下一次調整容器大小的閾值. threshold=capacity * load factor
HashMap的兩種節點
- 基本的哈希桶的節點(鏈表的結點) Node
static class Node<K,V> implements Map.Entry<K,V>
它繼承了Map的Entry,是對子類的行爲規範。要求提供了getKey(),getValue()等常用方法。
鏈表節點的結構如下:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash; // 避免重複計算key的hash值
final K key;
V value;
// 指向下一個節點的指針
HashMap.Node<K,V> next;
Node(int hash, K key, V value, HashMap.Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
@Override
public final K getKey() { return key; }
@Override
public final V getValue() { return value; }
@Override
public final String toString() { return key + "=" + value; }
// todo 沒有找到在哪裏使用了這個方法
@Override
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
@Override
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
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;
}
}
- Tree的節點 TreeNode
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V>
繼承了其子類的Entry, 子類的Entry繼承了父類的Node.注意了,這裏乍一看還挺亂。來張圖吧。
[外鏈圖片轉存失敗(img-ZPV5rtwT-1567747065189)(./images/hashMap的節點的繼承圖.png)]
這裏呢,TreeNode其實是Node的孫子, 也就是說HashMap的樹節點是鏈表節點的孫子輩兒的。
爲什麼要使兩種節點有繼承關係呢? 爲什麼TreeNode不直接繼承Node節點呢?
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
HashMap.TreeNode<K,V> parent; // red-black tree links
HashMap.TreeNode<K,V> left;
HashMap.TreeNode<K,V> right;
HashMap.TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, HashMap.Node<K,V> next) {
super(hash, key, val, next);
}
// 省略其他代碼
}
HashMap增加方法 HashMap#put()
/**
* 將指定的value和key關聯在map中。
* 如果map中已經存在了key,那麼將會替換掉老的value。
* @param key key 指定的key
* @param value value 和指定key關聯的value
* @return 如果返回了value,就說明map中原來和key關聯是有值的。如果返回null就說明沒有value。
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
這裏就比較有看點了, 1.這裏是hashMap的增加方法,增加方法裏必然會遇到hash衝突的問題,我們等會看下hash衝突是如何處理的,還會涉及到擴容的問題, 我們也要來看看他是怎麼擴容的, 擴容的過程中還會遇到普通的桶轉換成樹的過程.我們先來看下hash值是怎麼計算出來的。
- hash值的計算 {TODO 和jdk1.7中的比較}
/**
* 計算key的hashCode並且和hashCode值高16位進行異或運算。(異或: 相同爲0,不同爲1)
* 混和低位和高位,就是爲了加大低位的隨機性,而且混合後的低位摻雜了高位的部分特徵,
* 這樣高位的信息也被變相的保留了下來。
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
爲什麼這麼做呢? 見HashMap的Hash函數到底有什麼意義
- 那我們接下接着看putVal()方法。
/**
* 實現Map.put相關的方法。
* @param hash hash for key
* @param key the key
* @param value the value to put
* @param onlyIfAbsent if true, don't change existing value
* 如果是true的,不會修改存在的值。返回老的值。
* @param evict if false, the table is in creation mode.
* 如果爲false的時候,表屬於創建模式,第一次新增元素的時候。
* @return previous value, or null if none
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
HashMap.Node<K,V>[] tab;
HashMap.Node<K,V> p;
int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
// 如果數組爲null,或者數組長度爲0的時候,數組需要調整大小。
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
// 定位到數組的桶爲null的時候,創建桶內的第一個元素。next=null;
tab[i] = newNode(hash, key, value, null);
else {
// 如果桶不爲null,則創建鏈表
HashMap.Node<K,V> e; K k;
// p表示當前桶的第一個元素。
// 如果新增的元素和第一個元素相等的話(出現hash衝突),暫存已經存在的元素到變量e中。
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof HashMap.TreeNode)
// 如果是樹節點。
e = ((HashMap.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);
if (binCount >= TREEIFY_THRESHOLD - 1)
// 如果桶內的元素數量達到樹化的閾值,將鏈表轉換成樹。
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
// 如果第一個元素和要新增的元素hash,key都相等的話,直接進行新增操作。
break;
p = e;
}
}
if (e != null) { // existing mapping for key
// 如果原來的元素不爲空,保留原來的值。
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
// 覆蓋掉原來的value;
e.value = value;
// 留一個無方法體的方法,供子類擴展
afterNodeAccess(e);
return oldValue;
}
}
// failFast計數
++modCount;
if (++size > threshold)
// 如果table中的桶的數量超過了閾值。擴容。
resize();
// 供子類擴展的方法。
afterNodeInsertion(evict);
return null;
}
這段代碼裏中有三處重要的地方,resize(),treeifyBin(),putTreeNode(),接下來我們依次看下這三個方法。
resize
/**
* 初始化,或者加倍表格的大小
* 如果爲null時候,根據字段threshold的初始容量進行分配
* 否則,因爲我們正在使用二次冪擴展,所以每個bin中的元素必須保持相同的索引,或者在新表中以兩個偏移的冪移動
*
* @return the table 新的表
*/
final HashMap.Node<K, V>[] resize() {
HashMap.Node<K, V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
// 如果舊錶的大小大於0
if (oldCap >= MAXIMUM_CAPACITY) {
// hash表達到最大容量
threshold = Integer.MAX_VALUE;
return oldTab;
} else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY) {
// 如果翻倍後舊錶大小<最大表長度,並且舊錶長度>默認初始化長度。
// 擴容的閾值也翻倍。 還是等級 table.length*loadFactor
newThr = oldThr << 1; // double threshold
}
} else if (oldThr > 0) { // initial capacity was placed in threshold
// 舊錶長度<=0,舊的threshold>0,
// 就把threshold設置爲表長度。
newCap = oldThr;
} else { // zero initial threshold signifies using defaults
// 設置爲默認值。
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
// 如果新的擴縮容閾值等於0,設置新的擴縮容閾值爲新的容量*負載因子.
float ft = (float) newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ?
(int) ft : Integer.MAX_VALUE);
}
threshold = newThr;
// 重新創建新的hash表
@SuppressWarnings({"rawtypes", "unchecked"})
HashMap.Node<K, V>[] newTab = (HashMap.Node<K, V>[]) new HashMap.Node[newCap];
table = newTab;
// 如果舊錶不爲空,進行擴容.
// 否則(舊錶爲空)就進行初始化過程.
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
HashMap.Node<K, V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null) {
// 如果當前桶只有一個節點。
newTab[e.hash & (newCap - 1)] = e;
} else if (e instanceof HashMap.TreeNode) {
// 如果當前桶是棵紅黑樹
((HashMap.TreeNode<K, V>) e).split(this, newTab, j, oldCap);
} else { // preserve order
// 桶是鏈表,將該桶內的元素重新分配到表中。
HashMap.Node<K, V> loHead = null, loTail = null;
HashMap.Node<K, V> hiHead = null, hiTail = null;
HashMap.Node<K, V> next;
// 遍歷桶內的元素,將元素重新分配到hash表內的各個桶中。
// 具體的實現過程是: 將當前的元素的hash值和容量取&,如果>0,那就說明該元素應該分配到新的桶內。
// 桶的位置就是: oldCap+j.即桶原來容器+該元素所在的桶的下標。(hiHead所標識的位置)
// 反之如果hash值是==0的,那麼該元素就應該還在當前桶內。(loHead所標識的位置)
// 這裏所說的位置都是指桶的下標,整個表都是新的了,位置肯定都變了。
// 爲什麼可以這麼實現呢?
// 因爲擴容的時候,使用的是原來容量的2倍進行擴容的。所以就可以使用(oldCap+j)的方式來確定元素的新位置了。
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
// 還在原桶中
if (loTail == null)
loHead = e;
else {
// 位置最後一個節點爲空,使用e=next的時候,next爲null的情況。
// 在桶內元素遍歷完成後,會把桶的最後一個元素的next置爲null。
loTail.next = e;
}
loTail = e;
} else {
// 放置到新的桶內。
if (hiTail == null)
hiHead = e;
else {
// 位置最後一個節點爲空,使用e=next的時候,next爲null的情況。
// 在桶內元素遍歷完成後,會把桶的最後一個元素的next置爲null。
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;
}
看一個散列還算非常均勻的例子來看擴容過程。
[外鏈圖片轉存失敗(img-4b59ZB73-1567747065195)(./images/hashMap04-Put方法過程01.png)]
那麼進行擴容的過程是怎麼樣的呢?
[外鏈圖片轉存失敗(img-uOO7peRH-1567747065197)(./images/hashMap05-resize方法01.png)]
以元素1和12爲例,看擴容過程:
元素1的hash值爲49.(以hashMap計算hash值的方式得出。), 與15取&計算桶的下標爲1, 擴容後,與31取&,計算桶的下標爲17.所以擴容前位置是0,擴容後元素1的存放位置是17。
代碼中是怎麼完成這個過程的呢?
和擴容前hash表的容量取&,得 49 & 16 = 16 > 0
(代碼第86-96行), 新的桶的頭節點(對應代碼裏的hiHead)就是當前節點1,尾節點(hiTail)賦爲當前節點。然後進行下一次do...while
循環,處理節點12, 計算出節點12的hash值爲1569
,進行計算1569 & 16 = 0 == 0
原來桶的頭結點是節點12,尾節點也是節點12(對應着代碼第76-86行),這樣hitail和loTail均不爲null, 所以然後直接使用newTab[j] = loHead;
和 newTab[j + oldCap] = hiHead;
的方式確定桶的位置。這個案例裏,處理完節點12纔會確定桶的位置。因爲原來的表中下標爲1的桶中有兩個元素1和12.那桶裏只有一個元素的怎麼處理的呢?newTab[e.hash & (newCap - 1)] = e;
e是當前節點,newCap是新表的容量。
如果你想問爲什麼能使用
hash & olcCap==0?
來決定是newTab[j]
還是newTab[j+oldCap]
這種方式來確定新的桶的下標的話。 那麼原因就是擴容使用的是2次冪的方式,容量是原來容量的2倍.所以就可以使用hash & olcCap==0?
來判斷了。
這個例子呢,演示了擴容過程中的鏈表的新增和擴容過程。再回頭看resize方法,還有一種情況我們沒有分析過.那就是
...
else if (e instanceof HashMap.TreeNode) {
// 如果當前桶是棵紅黑樹
((HashMap.TreeNode<K, V>) e).split(this, newTab, j, oldCap);
}
...
/**
* 將原來樹桶中的節點拆分爲更低或更高的樹桶,如果太小的話就轉化成鏈表
* 只被resize方法調用
*
* @param map hash表
* @param tab 表中的指定的桶的頭結點(桶是一個棵樹)
* @param index 要拆分的hash表的節點
* @param bit the bit of hash to split on 要分裂的hash位
*/
final void split(HashMap<K, V> map, HashMap.Node<K, V>[] tab, int index, int bit) {
HashMap.TreeNode<K, V> b = this;
// Relink into lo and hi lists, preserving order
HashMap.TreeNode<K, V> loHead = null, loTail = null;
HashMap.TreeNode<K, V> hiHead = null, hiTail = null;
// lc代表的是原來的桶的元素的數量
// hc代表新的桶中的元素的數量, 用來和UNTREEIFY_THRESHOLD比較決定是否要轉換結構.
int lc = 0, hc = 0;
// 這裏還是當做鏈表去處理,把桶內的元素重新散列。
for (HashMap.TreeNode<K, V> e = b, next; e != null; e = next) {
next = (HashMap.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;
}
}
// 散列完後,判斷原來的桶(lo)和新的桶中的元素個數
// 然後決定轉換爲樹還是鏈表
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);
}
}
}
將樹重新穿換成鏈表的過程就比較簡單了:
/**
* Returns a list of non-TreeNodes replacing those linked from
* this node.
*/
final HashMap.Node<K, V> untreeify(HashMap<K, V> map) {
HashMap.Node<K, V> hd = null, tl = null;
for (HashMap.Node<K, V> q = this; q != null; q = q.next) {
// replacementNode:將TreeNode轉成Node
HashMap.Node<K, V> p = map.replacementNode(q, null);
if (tl == null)
hd = p;
else
tl.next = p;
tl = p;
}
return hd;
}
這裏就是和紅黑樹相關的內容了,這裏關鍵的是split調用了一個treeify的方法。這個方法同時也被treeifyBin調用了.所以treeify方法就和treeifyBin方法一塊分享。
順便提一嘴,他們有如下的關係:
[外鏈圖片轉存失敗(img-YyYNbt4W-1567747065200)(./images/hashMap06-紅黑樹相關方法調用關係.png)]
其中藍色的是紅黑樹的方法,黃色的是HashMap調用的方法。
treeifyBin
/**
* 將鏈表轉換成樹。
* 替換給定hash值的索引處的桶的所有節點,如果表太小(table.length小於64),就調整大小.這裏其實是對hash表的一種優化,防止因爲表長度太小而轉換成樹,造成性能浪費
* @param hash 用於確定桶的位置。
*/
final void treeifyBin(HashMap.Node<K, V>[] tab, int hash) {
int n, index;
// 鏈表的節點
HashMap.Node<K, V> e;
// 如果hash表爲空或者hash表的長度小於最小化的樹化容量(64),這時會重調整大小。
// 將容量擴大爲原來的兩倍。
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) {
resize();
} else if ((e = tab[index = (n - 1) & hash]) != null) {
`HashMap.TreeNode<K, V> hd = null, tl = null;
do {
// 構建一個樹的節點。
HashMap.TreeNode<K, V> p = replacementTreeNode(e, null);
// 如果尾爲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);
}`
}
}
其實呢,這個treeifyBin
方法還是做了一些將桶樹化的前置操作,然後將裝有TreeNode
節點的桶交給了treeify
方法去真正的轉換爲一棵紅黑樹。那我們接下來看下treeify
方法。注意這個方法定義在HashMap.TreeNode#treeify()
treeify()方法
/**
* Forms tree of the nodes linked from this node.
* 把該節點連接的所有節點組成一棵樹。(樹化的過程)
*/
final void treeify(HashMap.Node<K, V>[] tab) {
// 該棵樹的根節點。
HashMap.TreeNode<K, V> root = null;
// x是遍歷的每個節點。
for (HashMap.TreeNode<K, V> x = this, next; x != null; x = next) {
// 存下下一個節點。(指向下一個節點的指針)
next = (HashMap.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;
for (HashMap.TreeNode<K, V> p = root; ; ) {
// dir,負值和0爲左子樹,正值爲右子樹。
int dir, ph;
K pk = p.key;
/*************判斷節點在左子樹還是右子樹 -start***************/
// h爲當前節點的hash值。
// p是父節點, ph是父節點的hash值。
if ((ph = p.hash) > h) {
// 放在左子樹
dir = -1;
} else if (ph < h) {
// 放在又子樹
dir = 1;
}
//如果當前節點和父節點的hash值相等:
//如果節點的key實現了Comparable, 或者 父節點和當前節點的key爲一個。
else if ((kc == null && (kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
// k是當前節點的key,pk是父節點的key
// 根據hashMap定義的規則,判斷當前節點應該位於左子樹還是右子樹。
dir = tieBreakOrder(k, pk);
}
/*************判斷節點在左子樹還是右子樹 -end***************/
HashMap.TreeNode<K, V> xp = p;
// p==null,代表着遍歷到了葉子節點。
if ((p = (dir <= 0) ? p.left : p.right) == null) {
// xp是當前節點的父節點。
x.parent = xp;
if (dir <= 0){
xp.left = x;
} else {
xp.right = x;
}
// 平衡插入的紅黑樹(完成插入後,紅黑樹的性質可能被破壞,這裏進行重新平衡)
root = balanceInsertion(root, x);
break;
}
}
}
}
//確保紅黑樹的根節點是桶的第一個節點。
moveRootToFront(tab, root);
}
在這裏呢,有3個方法沒有仔細去說明,分別是 tieBreakOrder(),balanceInsertion() 和 moveRootToFront(tab, root),注意,這三個方法在下面的PutTreeVal中也有調用.當然包括調整平衡的左旋(rotateLeft),右旋(rotateRight)方法.我們接着往下看吧。
balanceInsertion方法
在說這個方法之前,先總結下紅黑樹變換的5條規則。
- 規則1: 紅黑樹爲空樹 ==> {直接插入當前節點,節點塗爲黑色。}
- 規則2: 插入節點的父節點是黑色 ==> {直接插入當前節點.}
- 規則3: 當前節點的父節點是紅色,並且叔叔節點是紅色。==> {父節點塗黑,叔叔節點塗黑,祖父節點塗紅.}
- 規則4: 當前節點的父節點是紅色,叔叔是黑色,當前節點是父節點的右子樹. ==> {當前節點的父節點作爲新的當前節點,以新的當前節點左旋。}
- 規則5: 當前節點的父節點是紅色,叔叔節點是黑色,當前節點是父節點的左子樹. ==> {父節點變爲黑色,祖父節點變爲紅色,以祖父節點爲支點右旋.}
下面結合代碼看HashMap是怎麼實現上面這個5個規則的:
/**
* 調整紅黑樹
* @param root 根節點
* @param x 當前節點
*/
static <K, V> HashMap.TreeNode<K, V> balanceInsertion(HashMap.TreeNode<K, V> root,
HashMap.TreeNode<K, V> x) {
x.red = true;
// xp: 當前節點的父節點(父節點)
// xpp: 當前節點的父節點的父節點(祖父節點)
// xppl: 當前節點的父節點的父節點的左子樹(叔叔節點)
// xppr: 當前節點的父節點的父節點的右子樹(叔叔節點)
for (HashMap.TreeNode<K, V> xp, xpp, xppl, xppr; ; ) {
// 規則1
if ((xp = x.parent) == null) {
x.red = false;
return x;
}
// 父節點爲黑色 或者祖父節點爲空==>規則2
else if (!xp.red || (xpp = xp.parent) == null) {
return root;
}
// 父節點是左子樹
if (xp == (xppl = xpp.left)) {
// 父節點是左子樹,且祖父節點存在右子樹(叔叔節點爲右子樹),並且叔叔爲紅色。 ==> 父節點是右子樹時的性質1.
if ((xppr = xpp.right) != null && xppr.red) {
// 叔叔節點塗黑
xppr.red = false;
// 父節點塗黑
xp.red = false;
// 祖父節點塗紅
xpp.red = true;
// 以祖父節點爲新的當前節點
x = xpp;
}
// 祖父節點沒有右子樹或者有右子樹,顏色爲黑色。
else {
// 當前節點是父節點的右子樹==> 規則4
if (x == xp.right) {
// 左旋
root = rotateLeft(root, x = xp);
// 設置祖父節點要麼爲空要麼是父節點。
xpp = (xp = x.parent) == null ? null : xp.parent;
}
// 規則5
if (xp != null) {
// 父節點塗成黑色
// 此時xp可能爲root.
xp.red = false;
// 如果xp不是root的時候。
if (xpp != null) {
// 祖父節點塗成紅色,右旋。
xpp.red = true;
root = rotateRight(root, xpp);
}
}
}
}
// 父節點不是左子樹==> 父節點是右子樹。
else {
// 叔叔節點(祖父節點的左子樹),叔叔爲紅色 ==> 規則3
if (xppl != null && xppl.red) {
// 叔叔塗黑
xppl.red = false;
// 父節點塗黑
xp.red = false;
// 祖父節點塗紅
xpp.red = true;
// 以祖父節點爲新的當前節點
x = xpp;
}
// 祖父節點沒有右子樹或者有右子樹,顏色爲黑色。 ==> 規則4
else {
// 當前節點是左子樹
if (x == xp.left) {
// 右旋
root = rotateRight(root, x = xp);
// 設置祖父節點要麼爲空要麼是父節點。
xpp = (xp = x.parent) == null ? null : xp.parent;
}
// ==> 規則5
if (xp != null) {
xp.red = false;
// 如果有祖父
if (xpp != null) {
// 祖父節點塗成紅色,右旋。
xpp.red = true;
root = rotateLeft(root, xpp);
}
}
}
}
}
}
rotateLeft 左旋
這裏的代碼不能用語言描述,真的是隻能意會不能言傳啊。
static <K, V> HashMap.TreeNode<K, V> rotateLeft2(HashMap.TreeNode<K, V> root, HashMap.TreeNode<K, V> p) {
HashMap.TreeNode<K, V> r, pp, rl;
// p是父節點
if (p != null && p.right != null) {
// 右孩子
r = p.right;
// 右孩子有左孩子的話.
if (r.left != null) {
// 右孩子變成右孩子的左孩子。即rl變成了p的右孩子。
p.right = r.left;
rl = r.left;
rl.parent = p;
// 注意此時r沒有關聯。
}
pp = p.parent;
// 如果p沒有有父節點的話。
if (p.parent == null) {
// 將r的父節點置爲null
r.parent = p.parent;
// 顏色塗成黑色,並且r就是根節點。
(root = r).red = false;
}
// 如果p節點有父節點,並且p是左子樹的話
else if (pp.left == p) {
// 將祖父節點的左子樹置爲r,
pp.left = r;
} else {
// 將祖父節點的右子樹置爲r,
pp.right = r;
}
// 將r和p連接起來。
r.left = p;
p.parent = r;
}
return root;
}
注意下,這裏的代碼是我修改之後,JDK的源碼看起來很精簡,理解起來,嘖嘖嘖。
MD,來張圖:
[外鏈圖片轉存失敗(img-HtJlVEms-1567747065203)(./images/HashMap07-左旋的過程.png)]
這裏假設右孩子是有左孩子的。如果沒有的話,那就直接去掉綠色的rl就好了。
rotateRight
右旋的過程同理:
static <K, V> HashMap.TreeNode<K, V> rotateRight(HashMap.TreeNode<K, V> root,
HashMap.TreeNode<K, V> p) {
HashMap.TreeNode<K, V> l, pp, lr;
if (p != null && (l = p.left) != null) {
if ((lr = p.left = l.right) != null)
lr.parent = p;
if ((pp = l.parent = p.parent) == null)
(root = l).red = false;
else if (pp.right == p)
pp.right = l;
else
pp.left = l;
l.right = p;
p.parent = l;
}
return root;
}
這圖啊,有空再做吧。今天太累了。
還有一個方法:
moveRootToFront
/**
* Ensures that the given root is the first node of its bin.
* // 確保紅黑樹的根節點是桶的第一個節點。
* 爲什麼不直接將tab[index]==root? 是爲了樹重新轉換成鏈表的時候使用的。
*/
static <K, V> void moveRootToFront(HashMap.Node<K, V>[] tab, HashMap.TreeNode<K, V> root) {
int n;
if (root != null && tab != null && (n = tab.length) > 0) {
int index = (n - 1) & root.hash;
HashMap.TreeNode<K, V> first = (HashMap.TreeNode<K, V>) tab[index];
// 判斷第一個節點和root是不是相等的,判斷的是地址。
if (root != first) {
HashMap.Node<K, V> rn;
tab[index] = root;
HashMap.TreeNode<K, V> rp = root.prev;
if ((rn = root.next) != null) {
// root的後一個節點的指向前的指針指向root的前一個節點。
((HashMap.TreeNode<K, V>) rn).prev = rp;
}
if (rp != null) {
// root的前一個節點的指向後的指針指向root的後一個節點。
rp.next = rn;
}
if (first != null) {
// 第一個元素的前指針指向root
first.prev = root;
}
// root的後向指針指向first
root.next = first;
// root的前向指針置爲null
root.prev = null;
}
// 遞歸不變檢查
assert checkInvariants(root);
}
}
putTreeNode
final HashMap.TreeNode<K, V> putTreeVal(HashMap<K, V> map, HashMap.Node<K, V>[] tab,
int h, K k, V v) {
Class<?> kc = null;
boolean searched = false;
HashMap.TreeNode<K, V> root = (parent != null) ? root() : this;
for (HashMap.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) {
if (!searched) {
HashMap.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;
}
dir = tieBreakOrder(k, pk);
}
/***************判斷 左右子樹 end******************/
HashMap.TreeNode<K, V> xp = p;
if ((p = (dir <= 0) ? p.left : p.right) == null) {
HashMap.Node<K, V> xpn = xp.next;
HashMap.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)
((HashMap.TreeNode<K, V>) xpn).prev = x;
// 這裏比較重要了,不過我們在treeify中已經說過了。
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
這樣,HashMap的新增過程我們就處理完了。
HashMap刪除方法 HashMap#remove()
/**
* 從map中刪除指定的key,如果key存在的話
* @param key key whose mapping is to be removed from the map
* @return value 如果key存在,返回key對應的Value,如果不存在返回null
*/
public V remove(Object key) {
HashMap.Node<K, V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
其中計算hash值的方法還是和之前的一樣。
removeNode
/**
* Implements Map.remove and related methods.
* 實現Map.remove相關的方法
* @param hash hashCode
* @param key key
* @param value value
* @param matchValue 如果是true,僅在value相等的時候刪除。
* @param movable 如果爲false,則在刪除節點的時候不移動其他節點。
* @return 返回刪除的節點
*/
final HashMap.Node<K, V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
HashMap.Node<K, V>[] tab;
HashMap.Node<K, V> p;
int n, index;
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
HashMap.Node<K, V> node = null, e;
K k;
V v;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
if (p instanceof HashMap.TreeNode) {
// 找到紅黑樹中的節點
node = ((HashMap.TreeNode<K, V>) p).getTreeNode(hash, key);
} else {
// 刪除鏈表中的節點1: 查找到節點的位置。
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
// 真正的去刪除的過程。
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof HashMap.TreeNode) {
// 刪除紅黑樹的節點
((HashMap.TreeNode<K, V>) node).removeTreeNode(this, tab, movable);
}else if (node == p) {
// 桶中只有當前的節點。
tab[index] = node.next;
}else {
// 鏈表中節點的刪除
p.next = node.next;
}
// 修改次數+1
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
還有一個最難理解的方法落在了紅黑樹的移除上了。
HashMap#TreeNode#removeTreeNode
還是先看下紅黑樹的刪除是怎麼回事。
在刪除方法調用之前必須要有存在的給定節點。
這比典型的紅黑刪除代碼更混亂,因爲我們不能將內部節點的內容與葉子後繼交換,後者由遍歷期間可獨立訪問的“下一個”指針固定。 所以我們交換樹鏈接。 如果當前樹似乎有太少的節點,則紅黑樹(bin)將轉換回普通的鏈表(普通bin). (測試會在2到6個節點之間觸發,具體取決於樹結構)。
上面是 removeTreeNode方法的解釋.說實話,沒理解…
HashMap的刪除不同於普通的紅黑樹的刪除, 因爲它其中還維護了,一個鏈表的指向. HashMap採用的是將樹中的兩個節點進行換位, 顏色也要進行互換,來保證紅黑樹的平衡,並不改變二者在鏈表中的位置,互換後,刪除節點此時的左子樹是空的,將問題轉換成了對左子樹爲空的節點的刪除。
有一個簡單的問題,千萬不要弄混了,就是TreeNode中要刪除的節點是誰??
刪除的簽名是這樣的:final void removeTreeNode(HashMap<K, V> map, HashMap.Node<K, V>[] tab,boolean movable)
,並沒有傳 TreeNode啊?是不是??
幹嗎呢!大兄嘚. 要刪除的節點是:this啊。我們現在走到了TreeNode內部了!! 它本身就是要被刪除的節點啊。
好了,那我現在要告訴你: 刪除自己!
HashMap刪除紅黑樹的節點,實際上就是 TreeNode自己刪除自己。那麼它是怎麼刪的呢?
它分成了三步:
- 1.將刪除節點從雙鏈向鏈表中刪除.
- 2.將刪除節點與其右子樹最小節點互換,之後平衡樹
- 3.將樹根節點,移動到
tab[index]
指針處
final void removeTreeNode(HashMap<K, V> map, HashMap.Node<K, V>[] tab,
boolean movable) {
// 注意了: 這個時候被刪除的節點是誰??
// 是this.
int n;
if (tab == null || (n = tab.length) == 0)
return;
// 找到對應的索引(確定對應桶的位置), n 是當前表的長度
int index = (n - 1) & hash;
// first: 第一個樹節點(當前爲父節點),root,父節點。rl:
HashMap.TreeNode<K, V> first = (HashMap.TreeNode<K, V>) tab[index], root = first, rl;
// succ:下一個節點(鏈表的指向)。pred, 前一個節點。
HashMap.TreeNode<K, V> succ = (HashMap.TreeNode<K, V>) next, pred = prev;
if (pred == null) {
// 前一個爲空時,即當前接是父節點:(被刪除的節點是根節點)
tab[index] = first = succ;
} else {
// 否測,前一個節點的下一個執行當前節點的下一個。(意會)
pred.next = succ;
}
if (succ != null) {
// 當前節點的後節點不爲null,後一個節點的前節點指向當前節點的前節點(意會)
succ.prev = pred;
}
if (first == null) {
// 如果刪除當前節點,該桶變成了null的。就直接返回
return;
}
if (root.parent != null) {
// 重置table[index]處爲樹的根節點。
root = root.root();
}
// PS: 說點沒用, JDK除了部分ifelse不加括號之外,
// 其實換行,還是用的挺多的,看起來也挺舒服的。
// 值得借鑑
if (root == null
|| (movable && (root.right == null
|| (rl = root.left) == null
|| rl.left == null))) {
// 樹太小了,將樹轉換成鏈表
tab[index] = first.untreeify(map); // too small
return;
}
/*****注意!!! 此時已經從雙向鏈表中刪除了, 第一步走完。******/
// p是待刪除的節點,pl當前節點的左孩子節點,pr當前節點的右孩子節點,replacement,用來交換的節點。
HashMap.TreeNode<K, V> p = this, pl = left, pr = right, replacement;
if (pl != null && pr != null) {
// s爲右子樹的最小的節點,sl爲左子樹(一下五行和源碼略有不同)
HashMap.TreeNode<K, V> s = pr, sl = s.left;
while (sl != null) { // find successor
s = sl;
sl = s.left;
}
// 交換顏色
boolean c = s.red;
s.red = p.red;
p.red = c; // swap colors
// 交換節點連接
HashMap.TreeNode<K, V> sr = s.right;
HashMap.TreeNode<K, V> pp = p.parent;
// pr是當前節點的右孩子節點
// s是當前節點的右子樹的最小的節點
// p的右子樹,只有s這一個節點
if (s == pr) { // p was s's direct parent
p.parent = s;
s.right = p;
} else { //
// sp: 最小節點的父節點
HashMap.TreeNode<K, V> sp = s.parent;
if ((p.parent = sp) != null) {
if (s == sp.left)
sp.left = p;
else
sp.right = p;
}
if ((s.right = pr) != null)
pr.parent = s;
}
// 置null孩子。
p.left = null;
if ((p.right = sr) != null) {
sr.parent = p;
}
if ((s.left = pl) != null) {
pl.parent = s;
}
if ((s.parent = pp) == null) {
root = s;
} else if (p == pp.left) {
pp.left = s;
} else {
pp.right = s;
}
// 確定要交換的節點完畢,交換節點
if (sr != null) {
replacement = sr;
} else {
replacement = p;
}
} else if (pl != null) {
// 當前樹只含有左子樹
replacement = pl;
} else if (pr != null) {
// 當前樹,只有又子樹
replacement = pr;
} else {
// 無孩子
replacement = p;
}
if (replacement != p) {
HashMap.TreeNode<K, V> pp = replacement.parent = p.parent;
if (pp == null)
root = replacement;
else if (p == pp.left)
pp.left = replacement;
else
pp.right = replacement;
p.left = p.right = p.parent = null;
}
// 是否要進行重平衡樹?
HashMap.TreeNode<K, V> r = p.red ? root : balanceDeletion(root, replacement);
// 在平衡後刪除該節點
if (replacement == p) { // detach
HashMap.TreeNode<K, V> pp = p.parent;
p.parent = null;
if (pp != null) {
if (p == pp.left)
pp.left = null;
else if (p == pp.right)
pp.right = null;
}
}
// 參數moveable控制是否刪除節點後確保樹的根節點爲鏈表的頭節點
if (movable) {
// 將樹根節點,移動到tab[index]指針處
moveRootToFront(tab, r);
}
}
這樣呢,整個刪除過程就完成了。
用官方中的話,比較混亂。尤其是涉及到紅黑樹的刪除,這部分內容。還是需要好好消化,消化的。
下面我們還剩下兩個內容:修改和查找
HashMap的修改方法
留坑~
HashMap的查找方法
留坑~
問題解答
- 如果我的HashMap的初始大小設置爲
[3|9|12]
,第一次擴容的時候,容量變爲了多少? 是如何進行擴容的? - (有毒的問題)假設Hash表的長度是32,已知某一個bin中的鏈表長度爲7,如果新增一個元素還是在該bin中的時,會進行什麼操作?
resize
還是treeifyBin
? 假設完成這個操作後該bin中元素數量沒變,又新增一個元素還是到該bin中,這時進行什麼操作?
總結
- 表中允許null的鍵和null值。
- 線程不同步,
- 不保證元素的順序。
網上常見面試問題彙總以及參考解答
冷門知識點
- failFast機制。
JDK變更歷史說明
課後娛樂
- java實現紅黑樹
- 自定義實現hashMap。
參考文檔&答謝
感受
- 註釋: 新字段要加註釋標註此字段的作用,該字段是什麼含義。
閱讀之前記錄
1.圖解。遇到問題,畫圖說明。
2.一定要有自己的理解。
3.對比其他版本JDK。
如果記得還行的話,就關注下 公衆號 呀 ,看最新看詳細的文章!