概述
JDK 1.8對HashMap進行了比較大的優化,底層實現由之前的“數組+鏈表”改爲“數組+鏈表+紅黑樹”,本文就HashMap的幾個常用的重要方法和JDK 1.8之前的死循環問題展開學習討論。JDK 1.8的HashMap的數據結構如下圖所示,當鏈表節點較少時仍然是以鏈表存在,當鏈表節點較多時(大於8)會轉爲紅黑樹。
本文地址:http://blog.csdn.net/v123411739/article/details/78996181
幾個點:
先了解以下幾個點,有利於更好的理解HashMap的源碼和閱讀本文。
- 頭節點指的是table表上索引位置的節點,也就是鏈表的頭節點。
- 根結點(root節點)指的是紅黑樹最上面的那個節點,也就是沒有父節點的節點。
- 紅黑樹的根結點不一定是索引位置的頭結點。
- 轉爲紅黑樹節點後,鏈表的結構還存在,通過next屬性維持,紅黑樹節點在進行操作時都會維護鏈表的結構,並不是轉爲紅黑樹節點,鏈表結構就不存在了。
- 在紅黑樹上,葉子節點也可能有next節點,因爲紅黑樹的結構跟鏈表的結構是互不影響的,不會因爲是葉子節點就說該節點已經沒有next節點。
- 源碼中一些變量定義:如果定義了一個節點p,則pl爲p的左節點,pr爲p的右節點,pp爲p的父節點,ph爲p的hash值,pk爲p的key值,kc爲key的類等等。源碼中很喜歡在if/for等語句中進行賦值並判斷,請注意。
- 鏈表中移除一個節點只需如下圖操作,其他操作同理。
- 紅黑樹在維護鏈表結構時,移除一個節點只需如下圖操作(紅黑樹中增加了一個prev屬性),其他操作同理。注:此處只是紅黑樹維護鏈表結構的操作,紅黑樹還需要單獨進行紅黑樹的移除或者其他操作。
- 源碼中進行紅黑樹的查找時,會反覆用到以下兩條規則:1)如果目標節點的hash值小於p節點的hash值,則向p節點的左邊遍歷;否則向p節點的右邊遍歷。2)如果目標節點的key值小於p節點的key值,則向p節點的左邊遍歷;否則向p節點的右邊遍歷。這兩條規則是利用了紅黑樹的特性(左節點<根結點<右節點)。
- 源碼中進行紅黑樹的查找時,會用dir(direction)來表示向左還是向右查找,dir存儲的值是目標節點的hash/key與p節點的hash/key的比較結果。
基本屬性
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默認容量16
/**
* The maximum capacity, used if a higher value is implicitly specified
* by either of the constructors with arguments.
* MUST be a power of two <= 1<<30.
*/
static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量
/**
* The load factor used when none specified in constructor.
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默認負載因子0.75
/**
* The bin count threshold for using a tree rather than list for a
* bin. Bins are converted to trees when adding an element to a
* bin with at least this many nodes. The value must be greater
* than 2 and should be at least 8 to mesh with assumptions in
* tree removal about conversion back to plain bins upon
* shrinkage.
*/
static final int TREEIFY_THRESHOLD = 8; // 鏈表節點轉換紅黑樹節點的閾值, 9個節點轉
/**
* The bin count threshold for untreeifying a (split) bin during a
* resize operation. Should be less than TREEIFY_THRESHOLD, and at
* most 6 to mesh with shrinkage detection under removal.
*/
static final int UNTREEIFY_THRESHOLD = 6; // 紅黑樹節點轉換鏈表節點的閾值, 6個節點轉
/**
* The smallest table capacity for which bins may be treeified.
* (Otherwise the table is resized if too many nodes in a bin.)
* Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
* between resizing and treeification thresholds.
*/
static final int MIN_TREEIFY_CAPACITY = 64; // 轉紅黑樹時, table的最小長度
/**
* Basic hash bin node, used for most entries. (See below for
* TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
*/
static class Node<K,V> implements Map.Entry<K,V> { // 基本hash節點, 繼承自Entry
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
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;
}
}
/**
* Entry for Tree bins. Extends LinkedHashMap.Entry (which in turn
* extends Node) so can be used as extension of either regular or
* linked node.
*/
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;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
// ...
}
定位哈希桶數組索引位置
不管增加、刪除、查找鍵值對,定位到哈希桶數組的位置都是很關鍵的第一步。前面說過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:
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;
// table不爲空 && table長度大於0 && table索引位置(根據hash值計算出)不爲空
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; // first的key等於傳入的key則返回first對象
if ((e = first.next) != null) { // 向下遍歷
if (first instanceof TreeNode) // 判斷是否爲TreeNode
// 如果是紅黑樹節點,則調用紅黑樹的查找目標節點方法getTreeNode
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
// 走到這代表節點爲鏈表節點
do { // 向下遍歷鏈表, 直至找到節點的key和傳入的key相等時,返回該節點
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null; // 找不到符合的返回空
}
- 先對table進行校驗,校驗是否爲空,length是否大於0
- 使用table.length - 1和hash值進行位與運算,得出在table上的索引位置,將該索引位置的節點賦值給first節點,校驗該索引位置是否爲空
- 檢查first節點的hash值和key是否和入參的一樣,如果一樣則first即爲目標節點,直接返回first節點
- 如果first的next節點不爲空則繼續遍歷
- 如果first節點爲TreeNode,則調用getTreeNode方法(見下文代碼塊1)查找目標節點
- 如果first節點不爲TreeNode,則調用普通的遍歷鏈表方法查找目標節點
- 如果查找不到目標節點則返回空
代碼塊1:getTreeNode方法
final TreeNode<K,V> getTreeNode(int h, Object k) {
// 使用根結點調用find方法
return ((parent != null) ? root() : this).find(h, k, null);
}
- 找到調用此方法的節點的樹的根節點
- 使用該樹的根節點調用find方法(見下文代碼塊2)
代碼塊2:find方法
/**
* 從調用此方法的結點開始查找, 通過hash值和key找到對應的節點
* 此處是紅黑樹的遍歷, 紅黑樹是特殊的自平衡二叉查找樹
* 平衡二叉查找樹的特點:左節點<根節點<右節點
*/
final TreeNode<K,V> find(int h, Object k, Class<?> kc) {
TreeNode<K,V> p = this; // this爲調用此方法的節點
do {
int ph, dir; K pk;
TreeNode<K,V> pl = p.left, pr = p.right, q;
if ((ph = p.hash) > h) // 傳入的hash值小於p節點的hash值, 則往p節點的左邊遍歷
p = pl; // p賦值爲p節點的左節點
else if (ph < h) // 傳入的hash值大於p節點的hash值, 則往p節點的右邊遍歷
p = pr; // p賦值爲p節點的右節點
// 傳入的hash值和key值等於p節點的hash值和key值,則p節點爲目標節點,返回p節點
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
else if (pl == null) // p節點的左節點爲空則將向右遍歷
p = pr;
else if (pr == null) // p節點的右節點爲空則向左遍歷
p = pl;
else if ((kc != null ||
// 如果傳入的key(k)所屬的類實現了Comparable接口,則將傳入的key跟p節點的key比較
(kc = comparableClassFor(k)) != null) && // 此行不爲空代表k實現了Comparable
(dir = compareComparables(kc, k, pk)) != 0)//k<pk則dir<0, k>pk則dir>0
p = (dir < 0) ? pl : pr; // k < pk則向左遍歷(p賦值爲p的左節點), 否則向右遍歷
// 代碼走到此處, 代表key所屬類沒有實現Comparable, 直接指定向p的右邊遍歷
else if ((q = pr.find(h, k, kc)) != null)
return q;
else// 代碼走到此處代表上一個向右遍歷(pr.find(h, k, kc))爲空, 因此直接向左遍歷
p = pl;
} while (p != null);
return null;
}
- 將p節點賦值爲調用此方法的節點
- 如果傳入的hash值小於p節點的hash值,則往p節點的左邊遍歷
- 如果傳入的hash值大於p節點的hash值,則往p節點的右邊遍歷
- 如果傳入的hash值等於p節點的hash值,並且傳入的key值跟p節點的key值相等, 則該p節點即爲目標節點,返回p節點
- 如果p的左節點爲空則向右遍歷,反之如果p的右節點爲空則向左遍歷
- 如果傳入的key(即代碼中的參數變量k)所屬的類實現了Comparable接口(kc不爲空,comparableClassFor方法見下文代碼塊3),則將傳入的key跟p節點的key進行比較(kc實現了Comparable接口,因此通過kc的比較方法進行比較),並將比較結果賦值給dir,如果dir<0則代表k<pk,則向p節點的左邊遍歷(pl);否則,向p節點的右邊遍歷(pr)。
- 代碼走到此處,代表key所屬類沒有實現Comparable,因此直接指定向p的右邊遍歷,如果能找到目標節點則返回
- 代碼走到此處代表與第7點向右遍歷沒有找到目標節點,因此直接向左邊遍歷
- 以上都找不到目標節點則返回空
代碼塊3:comparableClassFor方法
/**
* Returns x's Class if it is of the form "class C implements
* Comparable<C>", else null.
*/
static Class<?> comparableClassFor(Object x) {
if (x instanceof Comparable) {
Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
if ((c = x.getClass()) == String.class) // bypass checks
return c;
if ((ts = c.getGenericInterfaces()) != null) {
for (int i = 0; i < ts.length; ++i) {
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;
}
如果x實現了Comparable接口,則返回 x的Class。
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;
// table是否爲空或者length等於0, 如果是則調用resize方法進行初始化
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 通過hash值計算索引位置, 如果table表該索引位置節點爲空則新增一個
if ((p = tab[i = (n - 1) & hash]) == null)// 將索引位置的頭節點賦值給p
tab[i] = newNode(hash, key, value, null);
else { // table表該索引位置不爲空
Node<K,V> e; K k;
if (p.hash == hash && // 判斷p節點的hash值和key值是否跟傳入的hash值和key值相等
((k = p.key) == key || (key != null && key.equals(k))))
e = p; // 如果相等, 則p節點即爲要查找的目標節點,賦值給e
// 判斷p節點是否爲TreeNode, 如果是則調用紅黑樹的putTreeVal方法查找目標節點
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else { // 走到這代表p節點爲普通鏈表節點
for (int binCount = 0; ; ++binCount) { // 遍歷此鏈表, binCount用於統計節點數
if ((e = p.next) == null) { // p.next爲空代表不存在目標節點則新增一個節點插入鏈表尾部
p.next = newNode(hash, key, value, null);
// 計算節點是否超過8個, 減一是因爲循環是從p節點的下一個節點開始的
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);// 如果超過8個,調用treeifyBin方法將該鏈表轉換爲紅黑樹
break;
}
if (e.hash == hash && // e節點的hash值和key值都與傳入的相等, 則e即爲目標節點,跳出循環
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e; // 將p指向下一個節點
}
}
// e不爲空則代表根據傳入的hash值和key值查找到了節點,將該節點的value覆蓋,返回oldValue
if (e != null) {
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e); // 用於LinkedHashMap
return oldValue;
}
}
++modCount;
if (++size > threshold) // 插入節點後超過閾值則進行擴容
resize();
afterNodeInsertion(evict); // 用於LinkedHashMap
return null;
}
- 校驗table是否爲空或者length等於0,如果是則調用resize方法(見下文resize方法)進行初始化
- 通過hash值計算索引位置,將該索引位置的頭節點賦值給p節點,如果該索引位置節點爲空則使用傳入的參數新增一個節點並放在該索引位置
- 判斷p節點的key和hash值是否跟傳入的相等,如果相等, 則p節點即爲要查找的目標節點,將p節點賦值給e節點
- 如果p節點不是目標節點,則判斷p節點是否爲TreeNode,如果是則調用紅黑樹的putTreeVal方法(見下文代碼塊4)查找目標節點
- 走到這代表p節點爲普通鏈表節點,則調用普通的鏈表方法進行查找,並定義變量binCount來統計該鏈表的節點數
- 如果p的next節點爲空時,則代表找不到目標節點,則新增一個節點並插入鏈表尾部,並校驗節點數是否超過8個,如果超過則調用treeifyBin方法(見下文代碼塊6)將鏈表節點轉爲紅黑樹節點
- 如果遍歷的e節點存在hash值和key值都與傳入的相同,則e節點即爲目標節點,跳出循環
- 如果e節點不爲空,則代表目標節點存在,使用傳入的value覆蓋該節點的value,並返回oldValue
- 如果插入節點後節點數超過閾值,則調用resize方法(見下文resize方法)進行擴容
代碼塊4:putTreeVal方法
/**
* Tree version of putVal.
* 紅黑樹插入會同時維護原來的鏈表屬性, 即原來的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;
// 查找根節點, 索引位置的頭節點並不一定爲紅黑樹的根結點
TreeNode<K,V> root = (parent != null) ? root() : this;
for (TreeNode<K,V> p = root;;) { // 將根節點賦值給p, 開始遍歷
int dir, ph; K pk;
if ((ph = p.hash) > h) // 如果傳入的hash值小於p節點的hash值
dir = -1; // 則將dir賦值爲-1, 代表向p的左邊查找樹
else if (ph < h) // 如果傳入的hash值大於p節點的hash值,
dir = 1; // 則將dir賦值爲1, 代表向p的右邊查找樹
// 如果傳入的hash值和key值等於p節點的hash值和key值, 則p節點即爲目標節點, 返回p節點
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
// 如果k所屬的類沒有實現Comparable接口 或者 k和p節點的key相等
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
if (!searched) { // 第一次符合條件, 該方法只有第一次才執行
TreeNode<K,V> q, ch;
searched = true;
// 從p節點的左節點和右節點分別調用find方法進行查找, 如果查找到目標節點則返回
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;
}
// 否則使用定義的一套規則來比較k和p節點的key的大小, 用來決定向左還是向右查找
dir = tieBreakOrder(k, pk); // dir<0則代表k<pk,則向p左邊查找;反之亦然
}
TreeNode<K,V> xp = p; // xp賦值爲x的父節點,中間變量,用於下面給x的父節點賦值
// 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節點
// 創建新的節點, 其中x的next節點爲xpn, 即將x節點插入xp與xpn之間
TreeNode<K,V> x = map.newTreeNode(h, k, v, 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;
moveRootToFront(tab, balanceInsertion(root, x)); // 進行紅黑樹的插入平衡調整
return null;
}
}
}
- 查找當前紅黑樹的根結點,將根結點賦值給p節點,開始進行查找
- 如果傳入的hash值小於p節點的hash值,將dir賦值爲-1,代表向p的左邊查找樹
- 如果傳入的hash值大於p節點的hash值, 將dir賦值爲1,代表向p的右邊查找樹
- 如果傳入的hash值等於p節點的hash值,並且傳入的key值跟p節點的key值相等, 則該p節點即爲目標節點,返回p節點
- 如果k所屬的類沒有實現Comparable接口,或者k和p節點的key使用compareTo方法比較相等:第一次會從p節點的左節點和右節點分別調用find方法(見上文代碼塊2)進行查找,如果查找到目標節點則返回;如果不是第一次或者調用find方法沒有找到目標節點,則調用tieBreakOrder方法(見下文代碼塊5)比較k和p節點的key值的大小,以決定向樹的左節點還是右節點查找。
- 如果dir <= 0則向左節點查找(p賦值爲p.left,並進行下一次循環),否則向右節點查找,如果已經無法繼續查找(p賦值後爲null),則代表該位置即爲x的目標位置,另外變量xp用來記錄查找的最後一個節點,即下文新增的x節點的父節點。
- 以傳入的hash、key、value參數和xp節點的next節點爲參數,構建x節點(注意:xp節點在此處可能是葉子節點、沒有左節點的節點、沒有右節點的節點三種情況,即使它是葉子節點,它也可能有next節點,紅黑樹的結構跟鏈表的結構是互不影響的,不會因爲某個節點是葉子節點就說它沒有next節點,紅黑樹在進行操作時會同時維護紅黑樹結構和鏈表結構,next屬性就是用來維護鏈表結構的),根據dir的值決定x決定放在xp節點的左節點還是右節點,將xp的next節點設爲x,將x的parent和prev節點設爲xp,如果原xp的next節點(xpn)不爲空, 則將該節點的prev節點設置爲x節點, 與上面的將x節點的next節點設置爲xpn對應。
- 進行紅黑樹的插入平衡調整,見文末的解釋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;
// table爲空或者table的長度小於64, 進行擴容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
// 根據hash值計算索引值, 遍歷該索引位置的鏈表
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null); // 鏈表節點轉紅黑樹節點
if (tl == null) // tl爲空代表爲第一次循環
hd = p; // 頭結點
else {
p.prev = tl; // 當前節點的prev屬性設爲上一個節點
tl.next = p; // 上一個節點的next屬性設置爲當前節點
}
tl = p; // tl賦值爲p, 在下一次循環中作爲上一個節點
} while ((e = e.next) != null); // e指向下一個節點
// 將table該索引位置賦值爲新轉的TreeNode的頭節點
if ((tab[index] = hd) != null)
hd.treeify(tab); // 以頭結點爲根結點, 構建紅黑樹
}
}
- 校驗table是否爲空,如果長度小於64,則調用resize方法(見下文resize方法)進行擴容。
- 根據hash值計算索引值,將該索引位置的節點賦值給e節點,從e節點開始遍歷該索引位置的鏈表。
- 調用replacementTreeNode方法(該方法就一行代碼,直接返回一個新建的TreeNode)將鏈表節點轉爲紅黑樹節點,將頭結點賦值給hd節點,每次遍歷結束將p節點賦值給tl,用於在下一次循環中作爲上一個節點進行一些鏈表的關聯操作(p.prev = tl 和 tl.next = p)。
- 將table該索引位置賦值爲新轉的TreeNode的頭節點hd,如果該節點不爲空,則以hd爲根結點,調用treeify方法(見下文代碼塊7)構建紅黑樹。
代碼塊7:treeify方法
final void treeify(Node<K,V>[] tab) { // 構建紅黑樹
TreeNode<K,V> root = null;
for (TreeNode<K,V> x = this, next; x != null; x = next) {// this即爲調用此方法的TreeNode
next = (TreeNode<K,V>)x.next; // next賦值爲x的下個節點
x.left = x.right = null; // 將x的左右節點設置爲空
if (root == null) { // 如果還沒有根結點, 則將x設置爲根結點
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;
// 如果當前節點x不是根結點, 則從根節點開始查找屬於該節點的位置
for (TreeNode<K,V> p = root;;) {
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h) // 如果x節點的hash值小於p節點的hash值
dir = -1; // 則將dir賦值爲-1, 代表向p的左邊查找
else if (ph < h) // 與上面相反, 如果x節點的hash值大於p節點的hash值
dir = 1; // 則將dir賦值爲1, 代表向p的右邊查找
// 走到這代表x的hash值和p的hash值相等,則比較key值
else if ((kc == null && // 如果k沒有實現Comparable接口 或者 x節點的key和p節點的key相等
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
// 使用定義的一套規則來比較x節點和p節點的大小,用來決定向左還是向右查找
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p; // xp賦值爲x的父節點,中間變量用於下面給x的父節點賦值
// dir<=0則向p左邊查找,否則向p右邊查找,如果爲null,則代表該位置即爲x的目標位置
if ((p = (dir <= 0) ? p.left : p.right) == null) {
x.parent = xp; // x的父節點即爲最後一次遍歷的p節點
if (dir <= 0) // 如果時dir <= 0, 則代表x節點爲父節點的左節點
xp.left = x;
else // 如果時dir > 0, 則代表x節點爲父節點的右節點
xp.right = x;
// 進行紅黑樹的插入平衡(通過左旋、右旋和改變節點顏色來保證當前樹符合紅黑樹的要求)
root = balanceInsertion(root, x);
break;
}
}
}
}
moveRootToFront(tab, root); // 如果root節點不在table索引位置的頭結點, 則將其調整爲頭結點
}
- 從調用此方法的節點作爲起點,開始進行遍歷,並將此節點設爲root節點,標記爲黑色(x.red = false)。
- 如果當前節點不是根結點,則從根節點開始查找屬於該節點的位置(該段代碼跟之前的代碼塊2和代碼塊4的查找代碼類似)。
- 如果x節點(將要插入紅黑樹的節點)的hash值小於p節點(當前遍歷到的紅黑樹節點)的hash值,則向p節點的左邊查找。
- 與3相反,如果x節點的hash值大於p節點的hash值,則向p節點的右邊查找。
- 如果x的key沒有實現Comparable接口,或者x節點的key和p節點的key相等,使用tieBreakOrder方法(見上文代碼塊5)來比較x節點和p節點的大小,以決定向左還是向右查找(dir <= 0向左,否則向右)。
- 如果dir <= 0則向左節點查找(p賦值爲p.left,並進行下一次循環),否則向右節點查找,如果已經無法繼續查找(p賦值後爲null),則代表該位置即爲x的目標位置,另外變量xp用來記錄最後一個節點,即爲下文新增的x節點的父節點。
- 將x的父節點設置爲xp,根據dir的值決定x決定放在xp節點的左節點還是右節點,最後進行紅黑樹的插入平衡調整。
- 調用moveRootToFront方法(見下文代碼塊8)將root節點調整到索引位置的頭結點。
代碼塊8:moveRootToFront方法
/**
* 如果當前索引位置的頭節點不是root節點, 則將root的上一個節點和下一個節點進行關聯,
* 將root放到頭節點的位置, 原頭節點放在root的next節點上
*/
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) { // 如果root節點不是該索引位置的頭節點
Node<K,V> rn;
tab[index] = root; // 將該索引位置的頭節點賦值爲root節點
TreeNode<K,V> rp = root.prev; // root節點的上一個節點
// 如果root節點的下一個節點不爲空,
// 則將root節點的下一個節點的prev屬性設置爲root節點的上一個節點
if ((rn = root.next) != null)
((TreeNode<K,V>)rn).prev = rp;
// 如果root節點的上一個節點不爲空,
// 則將root節點的上一個節點的next屬性設置爲root節點的下一個節點
if (rp != null)
rp.next = rn;
if (first != null) // 如果原頭節點不爲空, 則將原頭節點的prev屬性設置爲root節點
first.prev = root;
root.next = first; // 將root節點的next屬性設置爲原頭節點
root.prev = null;
}
assert checkInvariants(root); // 檢查樹是否正常
}
}
- 校驗root是否爲空、table是否爲空、table的length是否大於0。
- 根據root節點的hash值計算出索引位置,判斷該索引位置的頭節點是否爲root節點,如果不是則進行以下操作將該索引位置的頭結點替換爲root節點。
- 將該索引位置的頭結點賦值爲root節點,如果root節點的next節點不爲空,則將root節點的next節點的prev屬性設置爲root節點的prev節點。
- 如果root節點的prev節點不爲空,則將root節點的prev節點的next屬性設置爲root節點的next節點(3和4兩個操作是一個完整的鏈表移除某個節點過程)。
- 如果原頭節點不爲空,則將原頭節點的prev屬性設置爲root節點
- 將root節點的next屬性設置爲原頭節點(5和6兩個操作將first節點接到root節點後面)
- root此時已經被放到該位置的頭結點位置,因此將prev屬性設爲空。
- 調用checkInvariants方法(見下文代碼塊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;
if (oldCap > 0) { // 老table不爲空
if (oldCap >= MAXIMUM_CAPACITY) { // 老table的容量超過最大容量值
threshold = Integer.MAX_VALUE; // 設置閾值爲Integer.MAX_VALUE
return oldTab;
}
// 如果容量*2<最大容量並且>=16, 則將閾值設置爲原來的兩倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // 老表的容量爲0, 老表的閾值大於0, 是因爲初始容量被放入閾值
newCap = oldThr; // 則將新表的容量設置爲老表的閾值
else { // 老表的容量爲0, 老表的閾值爲0, 則爲空表,設置默認容量和閾值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
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) { // 將索引值爲j的老表頭節點賦值給e
oldTab[j] = null; // 將老表的節點設置爲空, 以便垃圾收集器回收空間
// 如果e.next爲空, 則代表老表的該位置只有1個節點,
// 通過hash值計算新表的索引位置, 直接將該節點放在該位置
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
// 調用treeNode的hash分佈(跟下面最後一個else的內容幾乎相同)
((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; // 存儲索引位置爲:原索引+oldCap的節點
Node<K,V> next;
do {
next = e.next;
//如果e的hash值與老表的容量進行與運算爲0,則擴容後的索引位置跟老表的索引位置一樣
if ((e.hash & oldCap) == 0) {
if (loTail == null) // 如果loTail爲空, 代表該節點爲第一個節點
loHead = e; // 則將loHead賦值爲第一個節點
else
loTail.next = e; // 否則將節點添加在loTail後面
loTail = e; // 並將loTail賦值爲新增的節點
}
//如果e的hash值與老表的容量進行與運算爲1,則擴容後的索引位置爲:老表的索引位置+oldCap
else {
if (hiTail == null) // 如果hiTail爲空, 代表該節點爲第一個節點
hiHead = e; // 則將hiHead賦值爲第一個節點
else
hiTail.next = e; // 否則將節點添加在hiTail後面
hiTail = e; // 並將hiTail賦值爲新增的節點
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null; // 最後一個節點的next設爲空
newTab[j] = loHead; // 將原索引位置的節點設置爲對應的頭結點
}
if (hiTail != null) {
hiTail.next = null; // 最後一個節點的next設爲空
newTab[j + oldCap] = hiHead; // 將索引位置爲原索引+oldCap的節點設置爲對應的頭結點
}
}
}
}
}
return newTab;
}
- 如果老表的容量大於0,判斷老表的容量是否超過最大容量值:如果超過則將閾值設置爲Integer.MAX_VALUE,並直接返回老表(此時oldCap * 2比Integer.MAX_VALUE大,因此無法進行重新分佈,只是單純的將閾值擴容到最大);如果容量 * 2小於最大容量並且不小於16,則將閾值設置爲原來的兩倍。
- 如果老表的容量爲0,老表的閾值大於0,這種情況是傳了容量的new方法創建的空表,將新表的容量設置爲老表的閾值(這種情況發生在新創建的HashMap第一次put時,該HashMap初始化的時候傳了初始容量,由於HashMap並沒有capacity變量來存放容量值,因此傳進來的初始容量是存放在threshold變量上(查看HashMap(int initialCapacity, float loadFactor)方法),因此此時老表的threshold的值就是我們要新創建的HashMap的capacity,所以將新表的容量設置爲老表的閾值。
- 如果老表的容量爲0,老表的閾值爲0,這種情況是沒有傳容量的new方法創建的空表,將閾值和容量設置爲默認值。
- 如果新表的閾值爲空,則通過新的容量 * 負載因子獲得閾值(這種情況是初始化的時候傳了初始容量,跟第2點相同情況,也只有走到第2點纔會走到該情況)。
- 將當前閾值設置爲剛計算出來的新的閾值,定義新表,容量爲剛計算出來的新容量,將當前的表設置爲新定義的表。
- 如果老表不爲空,則需遍歷所有節點,將節點賦值給新表。
- 將老表上索引爲j的頭結點賦值給e節點,並將老表上索引爲j的節點設置爲空。
- 如果e的next節點爲空,則代表老表的該位置只有1個節點,通過hash值計算新表的索引位置,直接將該節點放在新表的該位置上。
- 如果e的next節點不爲空,並且e爲TreeNode,則調用split方法(見下文代碼塊10)進行hash分佈。
- 如果e的next節點不爲空,並且e爲普通的鏈表節點,則進行普通的hash分佈。
- 如果e的hash值與老表的容量(爲一串只有1個爲2的二進制數,例如16爲0000 0000 0001 0000)進行位與運算爲0,則說明e節點擴容後的索引位置跟老表的索引位置一樣(見例子1),進行鏈表拼接操作:如果loTail爲空,代表該節點爲第一個節點,則將loHead賦值爲該節點;否則將節點添加在loTail後面,並將loTail賦值爲新增的節點。
- 如果e的hash值與老表的容量(爲一串只有1個爲2的二進制數,例如16爲0000 0000 0001 0000)進行位與運算爲1,則說明e節點擴容後的索引位置爲:老表的索引位置+oldCap(見例子1),進行鏈表拼接操作:如果hiTail爲空,代表該節點爲第一個節點,則將hiHead賦值爲該節點;否則將節點添加在hiTail後面,並將hiTail賦值爲新增的節點。
- 老表節點重新hash分佈在新表結束後,如果loTail不爲空(說明老表的數據有分佈到新表上原索引位置的節點),則將最後一個節點的next設爲空,並將新表上原索引位置的節點設置爲對應的頭結點;如果hiTail不爲空(說明老表的數據有分佈到新表上原索引+oldCap位置的節點),則將最後一個節點的next設爲空,並將新表上索引位置爲原索引+oldCap的節點設置爲對應的頭結點。
- 返回新表。
代碼塊10:split方法
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;
for (TreeNode<K,V> e = b, next; e != null; e = next) { // 從b節點開始遍歷
next = (TreeNode<K,V>)e.next; // next賦值爲e的下個節點
e.next = null; // 同時將老表的節點設置爲空,以便垃圾收集器回收
//如果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; // 統計原索引位置的節點個數
}
//如果e的hash值與老表的容量進行與運算爲1,則擴容後的索引位置爲:老表的索引位置+oldCap
else {
if ((e.prev = hiTail) == null) // 如果hiHead爲空, 代表該節點爲第一個節點
hiHead = e; // 則將hiHead賦值爲第一個節點
else
hiTail.next = e; // 否則將節點添加在hiTail後面
hiTail = e; // 並將hiTail賦值爲新增的節點
++hc; // 統計索引位置爲原索引+oldCap的節點個數
}
}
if (loHead != null) { // 原索引位置的節點不爲空
if (lc <= UNTREEIFY_THRESHOLD) // 節點個數少於6個則將紅黑樹轉爲鏈表結構
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead; // 將原索引位置的節點設置爲對應的頭結點
// hiHead不爲空則代表原來的紅黑樹(老表的紅黑樹由於節點被分到兩個位置)
// 已經被改變, 需要重新構建新的紅黑樹
if (hiHead != null)
loHead.treeify(tab); // 以loHead爲根結點, 構建新的紅黑樹
}
}
if (hiHead != null) { // 索引位置爲原索引+oldCap的節點不爲空
if (hc <= UNTREEIFY_THRESHOLD) // 節點個數少於6個則將紅黑樹轉爲鏈表結構
tab[index + bit] = hiHead.untreeify(map);
else {
tab[index + bit] = hiHead; // 將索引位置爲原索引+oldCap的節點設置爲對應的頭結點
// loHead不爲空則代表原來的紅黑樹(老表的紅黑樹由於節點被分到兩個位置)
// 已經被改變, 需要重新構建新的紅黑樹
if (loHead != null)
hiHead.treeify(tab); // 以hiHead爲根結點, 構建新的紅黑樹
}
}
}
- 以調用此方法的節點開始,遍歷整個紅黑樹節點(此處實際是遍歷的鏈表節點,上文提過,紅黑樹節點也會同時維護鏈表結構)。
- 如果e的hash值與老表的容量(爲一串只有1個爲2的二進制數,例如16爲0000 0000 0001 0000)進行位與運算爲0,則說明e節點擴容後的索引位置跟老表的索引位置一樣(見下文例子1),進行鏈表拼接操作:如果loTail爲空,代表該節點爲第一個節點,則將loHead賦值爲該節點;否則將節點添加在loTail後面,並將loTail賦值爲新增的節點,並統計原索引位置的節點個數。
- 如果e的hash值與老表的容量(爲一串只有1個爲2的二進制數,例如16爲0000 0000 0001 0000)進行位與運算爲1,則說明e節點擴容後的索引位置爲:老表的索引位置+oldCap(見例子1),進行鏈表拼接操作:如果hiTail爲空,代表該節點爲第一個節點,則將hiHead賦值爲該節點;否則將節點添加在hiTail後面,並將hiTail賦值爲新增的節點,並統計索引位置爲原索引+oldCap的節點個數。
- 如果原索引位置的節點不爲空:如果當該索引位置節點數<=6個,調用untreeify方法(見下文代碼塊11)將紅黑樹節點轉爲鏈表節點;否則將原索引位置的節點設置爲對應的頭結點(即loHead結點),如果判斷hiHead不爲空則代表原來的紅黑樹(老表的紅黑樹由於節點被分到兩個位置)已經被改變,需要重新構建新的紅黑樹,以loHead爲根結點,調用treeify方法(見上文代碼塊7)構建新的紅黑樹。
- 如果索引位置爲原索引+oldCap的節點不爲空:如果當該索引位置節點數<=6個,調用untreeify方法(見下文代碼塊11)將紅黑樹節點轉爲鏈表節點;否則將索引位置爲原索引+oldCap的節點設置爲對應的頭結點(即hiHead結點),如果判斷loHead不爲空則代表原來的紅黑樹(老表的紅黑樹由於節點被分到兩個位置)已經被改變,需要重新構建新的紅黑樹,以hiHead爲根結點,調用treeify方法(見上文代碼塊7)構建新的紅黑樹。
代碼塊11:untreeify方法
// 將紅黑樹節點轉爲鏈表節點, 當節點<=6個時會被觸發
final Node<K,V> untreeify(HashMap<K,V> map) {
Node<K,V> hd = null, tl = null; // hd指向頭結點, tl指向尾節點
// 從調用該方法的節點, 即鏈表的頭結點開始遍歷, 將所有節點全轉爲鏈表節點
for (Node<K,V> q = this; q != null; q = q.next) {
// 調用replacementNode方法構建鏈表節點
Node<K,V> p = map.replacementNode(q, null);
// 如果tl爲null, 則代表當前節點爲第一個節點, 將hd賦值爲該節點
if (tl == null)
hd = p;
else // 否則, 將尾節點的next屬性設置爲當前節點p
tl.next = p;
tl = p; // 每次都將tl節點指向當前節點, 即尾節點
}
return hd; // 返回轉換後的鏈表的頭結點
}
- 從調用該方法的節點,即鏈表的頭結點開始遍歷, 將所有節點全轉爲鏈表節點
- 調用replacementNode方法構建鏈表節點
- 如果tl爲null, 則代表當前節點爲第一個節點,將hd賦值爲該節點,否則, 將尾節點的next屬性設置爲當前節點p
- 每次都將tl節點指向當前節點, 即尾節點
- 返回轉換後的鏈表的頭結點
例子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;
// 如果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;
// 如果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) { // 否則向下遍歷節點
if (p instanceof TreeNode) // 如果p是TreeNode則調用紅黑樹的方法查找節點
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do { // 遍歷鏈表查找符合條件的節點
// 當節點的hash值和key與傳入的相同,則該節點即爲目標節點
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e; // 賦值給node, 並跳出循環
break;
}
p = e; // p節點賦值爲本次結束的e
} while ((e = e.next) != null); // 指向像一個節點
}
}
// 如果node不爲空(即根據傳入key和hash值查找到目標節點),則進行移除操作
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode) // 如果是TreeNode則調用紅黑樹的移除方法
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
// 走到這代表節點是普通鏈表節點
// 如果node是該索引位置的頭結點則直接將該索引位置的值賦值爲node的next節點
else if (node == p)
tab[index] = node.next;
// 否則將node的上一個節點的next屬性設置爲node的next節點,
// 即將node節點移除, 將node的上下節點進行關聯(鏈表的移除)
else
p.next = node.next;
++modCount; // 修改次數+1
--size; // table的總節點數-1
afterNodeRemoval(node); // 供LinkedHashMap使用
return node; // 返回被移除的節點
}
}
return null;
}
- 如果table不爲空並且根據hash值計算出來的索引位置的值不爲空,將該位置的節點賦值給p。
- 如果p節點的hash值和key都與傳入的相同,則p即爲目標節點,賦值給node。
- 向下遍歷節點,如果p是TreeNode則調用getTreeNode方法(見上文代碼塊1)查找節點,並賦值給node。
- 遍歷鏈表查找符合條件的節點,當節點的hash值和key與傳入的值相同,則該節點即爲目標節點, 賦值給node,並跳出循環。
- 如果node不爲空,即根據傳入key和hash值查找到目標節點,判斷node是否爲TreeNode,如果是則調用紅黑樹的移除方法removeTreeNode方法(見下文代碼塊12)。
- 如果node是該索引位置的頭結點則直接將該索引位置的值賦值爲node節點的next節點。
- 否則將node的上一個節點(p節點)的next節點設置爲node的next節點,即將node節點移除,將node的上下節點進行關聯(鏈表的移除,可以看開頭的第7點)。
代碼塊12:removeTreeNode方法
這塊代碼比較長,目的就是移除調用此方法的節點,也就是該方法中的this節點。移除包括鏈表的處理和紅黑樹的處理。可以結合下文的圖解理解。
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab,
boolean movable) {
// 鏈表的處理start
int n;
if (tab == null || (n = tab.length) == 0) // table爲空或者length爲0直接返回
return;
int index = (n - 1) & hash; // 根據hash計算出索引的位置
// 索引位置的頭結點賦值給first和root
TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl;
// 該方法被將要被移除的node(TreeNode)調用, 因此此方法的this爲要被移除node節點,
// 則此處next即爲node的next節點, prev即爲node的prev節點
TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev;
if (pred == null) // 如果node節點的prev節點爲空
// 則將table索引位置的值和first節點的值賦值爲succ節點(node的next節點)即可
tab[index] = first = succ;
else
// 否則將node的prev節點的next屬性設置爲succ節點(node的next節點)(鏈表的移除)
pred.next = succ;
if (succ != null) // 如果succ節點不爲空
succ.prev = pred; // 則將succ的prev節點設置爲pred, 與上面對應
if (first == null) // 如果此處first爲空, 則代表該索引位置已經沒有節點則直接返回
return;
// 如果root的父節點不爲空, 則將root賦值爲根結點
// (root在上面被賦值爲索引位置的頭結點, 索引位置的頭節點並不一定爲紅黑樹的根結點)
if (root.parent != null)
root = root.root();
// 通過root節點來判斷此紅黑樹是否太小, 如果是則調用untreeify方法轉爲鏈表節點並返回
// (轉鏈表後就無需再進行下面的紅黑樹處理)
if (root == null || root.right == null ||
(rl = root.left) == null || rl.left == null) {
tab[index] = first.untreeify(map); // too small
return;
}
// 鏈表的處理end
// 以下代碼爲紅黑樹的處理, 上面的代碼已經將鏈表的部分處理完成
// 上面已經說了this爲要被移除的node節點,
// 將p賦值爲node節點,pl賦值爲node的左節點,pr賦值爲node的右節點
TreeNode<K,V> p = this, pl = left, pr = right, replacement;
if (pl != null && pr != null) { // node的左節點和右節點都不爲空時
TreeNode<K,V> s = pr, sl; // s節點賦值爲node的右節點
while ((sl = s.left) != null)//向左一直查找,直到葉子節點,跳出循環時,s爲葉子節點
s = sl;
boolean c = s.red; s.red = p.red; p.red = c; //交換p節點和s節點(葉子節點)的顏色
TreeNode<K,V> sr = s.right; // s的右節點
TreeNode<K,V> pp = p.parent; // p的父節點
// 第一次調整start
if (s == pr) { // 如果p節點的右節點即爲葉子節點
p.parent = s; // 將p的父節點賦值爲s
s.right = p; // 將s的右節點賦值爲p
}
else {
TreeNode<K,V> sp = s.parent;
if ((p.parent = sp) != null) { // 將p的父節點賦值爲s的父節點, 如果sp不爲空
if (s == sp.left) // 如果s節點爲左節點
sp.left = p; // 則將s的父節點的左節點賦值爲p節點
else // 如果s節點爲右節點
sp.right = p; // 則將s的父節點的右節點賦值爲p節點
}
if ((s.right = pr) != null) // s的右節點賦值爲p節點的右節點
pr.parent = s; // p節點的右節點的父節點賦值爲s
}
// 第二次調整start
p.left = null;
if ((p.right = sr) != null) // 將p節點的右節點賦值爲s的右節點, 如果sr不爲空
sr.parent = p; // 則將s右節點的父節點賦值爲p節點
if ((s.left = pl) != null) // 將s節點的左節點賦值爲p的左節點, 如果pl不爲空
pl.parent = s; // 則將p左節點的父節點賦值爲s節點
if ((s.parent = pp) == null) // 將s的父節點賦值爲p的父節點pp, 如果pp爲空
root = s; // 則p節點爲root節點, 此時交換後s成爲新的root節點
else if (p == pp.left) // 如果p不爲root節點, 並且p是父節點的左節點
pp.left = s; // 將p父節點的左節點賦值爲s節點
else // 如果p不爲root節點, 並且p是父節點的右節點
pp.right = s; // 將p父節點的右節點賦值爲s節點
if (sr != null)
replacement = sr; // 尋找replacement節點(用來替換掉p節點)
else
replacement = p; // 尋找replacement節點
}
else if (pl != null) // 如果p的左節點不爲空,右節點爲空,replacement節點爲p的左節點
replacement = pl;
else if (pr != null) // 如果p的右節點不爲空,左節點爲空,replacement節點爲p的右節點
replacement = pr;
else // 如果p的左右節點都爲空, 即p爲葉子節點, 替換節點爲p節點本身
replacement = p;
// 第三次調整start
if (replacement != p) { // 如果p節點不是葉子節點
//將replacement節點的父節點賦值爲p節點的父節點, 同時賦值給pp節點
TreeNode<K,V> pp = replacement.parent = p.parent;
if (pp == null) // 如果p節點沒有父節點, 即p爲root節點
root = replacement; // 則將root節點賦值爲replacement節點即可
else if (p == pp.left) // 如果p節點不是root節點, 並且p節點爲父節點的左節點
pp.left = replacement; // 則將p父節點的左節點賦值爲替換節點
else // 如果p節點不是root節點, 並且p節點爲父節點的右節點
pp.right = replacement; // 則將p父節點的右節點賦值爲替換節點
// p節點的位置已經被完整的替換爲替換節點, 將p節點清空, 以便垃圾收集器回收
p.left = p.right = p.parent = null;
}
// 如果p節點不爲紅色則進行紅黑樹刪除平衡調整
// (如果刪除的節點是紅色則不會破壞紅黑樹的平衡無需調整)
TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);
if (replacement == p) { // 如果p節點爲葉子節點, 則簡單的將p節點去除即可
TreeNode<K,V> pp = p.parent; // pp賦值爲p節點的父節點
p.parent = null; // 將p的parent節點設置爲空
if (pp != null) { // 如果p的父節點存在
if (p == pp.left) // 如果p節點爲父節點的左節點
pp.left = null; // 則將父節點的左節點賦值爲空
else if (p == pp.right) // 如果p節點爲父節點的右節點
pp.right = null; // 則將父節點的右節點賦值爲空
}
}
if (movable)
moveRootToFront(tab, r); // 將root節點移到索引位置的頭結點
}
- 如果table爲空或者length爲0直接返回。
- 根據hash值和length-1位於運算計算出索引的位置。
- 將索引位置的頭結點賦值給first和root,removeTreeNode方法是被將要移除的節點node調用,因此removeTreeNode方法裏的this即爲將要被移除的節點node,將node的next節點賦值給succ節點,prev節點賦值給pred節點。
- 如果node節點的prev節點爲空,則代表要被移除的node節點爲頭結點,則將table索引位置的值和first節點的值賦值爲node的next節點(succ節點)即可。
- 否則將node的prev節點(pred節點)的next節點設置爲node的next節點(succ節點),如果succ節點不爲空,則將succ的prev節點設置爲pred,與前面對應(TreeNode鏈表的移除,見開頭第8點)。
- 如果進行到此first節點爲空,則代表該索引位置已經沒有節點則直接返回。
- 如果root的父節點不爲空,則將root賦值爲根結點(root在上面被賦值爲索引位置的頭結點,索引位置的頭節點並不一定爲紅黑樹的根結點)。
- 通過root節點來判斷此紅黑樹是否太小,如果太小則轉爲鏈表節點並返回(轉鏈表後就無需再進行下面的紅黑樹處理),鏈表維護部分到此結束,此前的代碼說明了,紅黑樹在進行移除的同時也會維護鏈表結構,之後的代碼爲紅黑樹的移除節點處理。
- 上面已經說了this爲將要被移除的node節點,將p節點賦值爲將要被移除的node節點(則此時p節點就是我們要移除的節點),pl賦值爲node的左節點, pr賦值爲node的右節點(方法的命令見開頭第6點),replacement變量用來存儲將要替換掉被移除的node節點。
- 如果p的左節點和右節點都不爲空時,s節點賦值爲p的右節點;向s的左節點一直向左查找, 直到葉子節點,跳出循環時,s爲葉子節點;交換p節點和s節點(葉子節點)的顏色(此文下面的所有操作都是爲了實現將p節點和s節點進行位置調換,因此此處先將顏色替換);sr賦值爲s節點的右節點,pp節點賦值爲p節點的父節點(命令規律見文章開頭第6點)。
- PS:下面的第一次調整和第二次調整是將p節點和s節點進行了位置調換,然後找出要替換掉p節點的replacement;第三次調整是將replacement節點覆蓋掉p節點;這部分的代碼邏輯比較不容易理解透,建議自己動手畫圖模擬。(下文圖解1即爲這三次調整的例子)
- 進行第一次調整:如果p節點的右節點即爲葉子節點,將p的父節點賦值爲s,將s的右節點賦值爲p即可;否則,將p的父節點賦值爲s的父節點sp,並判斷sp是否爲空,如果不爲空,並判斷s是sp的左節點還是右節點,將s節點替換爲p節點;將s的右節點賦值爲p節點的右節點pr,如果pr不爲空則將pr的父節賦值爲s節點。
- 進行第二次調整:將p節點的左節點清空(上文pl已經保存了該節點);將p節點的右節點賦值爲s的右節點sr,如果sr不爲空,則將sr的父節點賦值爲p節點;將s節點的左節點賦值爲p的左節點pl,如果pl不爲空,則將p左節點的父節點賦值爲s節點;將s的父節點賦值爲p的父節點pp,如果pp爲空,則p節點爲root節點,此時交換後s成爲新的root節點,將root賦值爲s節點;如果p不爲root節點,並且p是父節點的左節點,將p父節點的左節點賦值爲s節點;如果p不爲root節點,並且p是父節點的右節點,將p父節點的右節點賦值爲s節點;如果sr不爲空,將replacement賦值爲sr節點,否則賦值爲p節點(爲什麼sr是replacement的首選,p爲備選?見解釋1)。
- 承接第10點的判斷,第10點~第12點爲p的左右節點都不爲空的情況需要進行的處理;如果p的左節點不爲空,右節點爲空,將replacement賦值爲p的左節點即可;如果p的右節點不爲空,左節點爲空,將replacement賦值爲p的右節點即可;如果p的左右節點都爲空,即p爲葉子節點, 將replacement賦值爲p節點本身。
- 進行第三次調整:如果p節點不是replacement(即p不是葉子節點),將replacement的父節點賦值爲p的父節點,同事賦值給pp節點;如果pp爲空(p節點沒有父節點),即p爲root節點,則將root節點賦值爲replacement節點即可;如果p節點不是root節點,並且p節點爲父節點的左節點,則將p父節點的左節點賦值爲replacement節點;如果p節點不是root節點,並且p節點爲父節點的右節點,則將p父節點的右節點賦值爲replacement節點;p節點的位置已經被完整的替換爲replacement節點, 將p節點清空。
- 如果p節點不爲紅色則進行紅黑樹刪除平衡調整(如果刪除的節點是紅色則不會破壞紅黑樹的平衡無需調整,見文末的解釋2)。
- 如果p節點爲葉子節點,則簡單的將p節點移除:將pp賦值爲p節點的父節點,將p的parent節點設置爲空,如果p的父節點pp存在,如果p節點爲父節點的左節點,則將父節點的左節點賦值爲空,如果p節點爲父節點的右節點,則將父節點的右節點賦值爲空。
- 如果movable爲true,則調用moveRootToFront方法(見上文代碼塊8)將root節點移到索引位置的頭結點。
解釋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節點有右節點。
解釋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後索引位置index的節點的新分佈位置最多隻有兩個:原索引位置或原索引+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個時,會觸發鏈表節點(Node)轉紅黑樹節點(TreeNode,間接繼承Node),轉成紅黑樹節點後,其實鏈表的結構還存在,通過next屬性維持。鏈表節點轉紅黑樹節點的具體方法爲源碼中的treeifyBin(Node<K,V>[] tab, int hash)方法。
- 當同一個索引位置的節點在移除後達到6個時,並且該索引位置的節點爲紅黑樹節點,會觸發紅黑樹節點轉鏈表節點。紅黑樹節點轉鏈表節點的具體方法爲源碼中的untreeify(HashMap<K,V> map)方法。
- HashMap在JDK1.8之後不再有死循環的問題,JDK1.8之前存在死循環的根本原因是在擴容後同一索引位置的節點順序會反掉。
- HashMap是非線程安全的,在併發場景下使用ConcurrentHashMap來代替。
參考
HashMap源碼(JDK 1.8)
—————END—————