首先放上參考的博客
https://blog.csdn.net/v123411739/article/details/78996181
jdk1.8之前 的hashMap 是基於數組加鏈表的形式的,jdk1.8 oracleJdk優化了jdk的源碼 採用數組加鏈表 或者數組加紅黑樹的形式 在鏈表上掛的數據超過一定長度後就會轉爲紅黑樹 。
我先搬上面博客的一點內容:
幾個點:
先了解以下幾個點,有利於更好的理解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的比較結果。
HashMap的基本數據結構
/**
* 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的最小長度
/* ---------------- Fields -------------- */
/**
* The table, initialized on first use, and resized as
* necessary. When allocated, length is always a power of two.
* (We also tolerate length zero in some operations to allow
* bootstrapping mechanics that are currently not needed.)
*/
transient Node<K,V>[] table; // node數組 也就是上面的 數組加鏈表的數組
/**
* Holds cached entrySet(). Note that AbstractMap fields are used
* for keySet() and values().
*/
transient Set<Map.Entry<K,V>> entrySet; // 內部key的set集合
/**
* The number of key-value mappings contained in this map.
*/
transient int size; //map大小
/**
* The number of times this HashMap has been structurally modified
* Structural modifications are those that change the number of mappings in
* the HashMap or otherwise modify its internal structure (e.g.,
* rehash). This field is used to make iterators on Collection-views of
* the HashMap fail-fast. (See ConcurrentModificationException).
*/
transient int modCount; //操作此時
/**
* The next size value at which to resize (capacity * load factor).
* 要調整大小的下一個大小值(容量*加載因子)。
* @serial
*/
// (The javadoc description is true upon serialization.
// Additionally, if the table array has not been allocated, this
// field holds the initial array capacity, or zero signifying
// DEFAULT_INITIAL_CAPACITY.)
int threshold;
/**
* The load factor for the hash table.
* 負載因子
* @serial
*/
final float loadFactor;
/* ---------------- Public operations -------------- */
/**
* 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)進行&運算
代碼1 是hashMap的 核心方法,代碼2 和代碼3是 get 和 put 時定位 當前node 的 位置 也就是上面 定義的 Node[] table 的索引值。
這位老哥寫的太好 我都不知道能補充啥。
方法解讀:
對於任意給定的對象,只要它的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) { // 這裏的 tab[(n - 1) & hash] 就是取到 當前key在 Node數組中所在索引的位置
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; // 找不到符合的返回空
}
1.先對table進行校驗,校驗是否爲空,length是否大於0
2.使用table.length - 1和hash值進行位與運算,得出在table上的索引位置,將該索引位置的節點賦值給first節點,校驗該索引位置是否爲空
3.檢查first節點的hash值和key是否和入參的一樣,如果一樣則first即爲目標節點,直接返回first節點
4.如果first的next節點不爲空則繼續遍歷
5.如果first節點爲TreeNode,則調用getTreeNode方法(見下文代碼塊1)查找目標節點
6.如果first節點不爲TreeNode,則調用普通的遍歷鏈表方法查找目標節點
7.如果查找不到目標節點則返回空
第一次看的時候一臉懵逼,但是等過了幾個月 看過了很多開源框架後,現在再來看 覺得也就那樣,所有還是要多看多學習,纔能有進步。
這裏對於查鏈表來說 很簡單,不需要解釋,但是對於該索引上的Node 是一顆紅黑樹的情況 來說 就比較複雜了。
final TreeNode<K,V> getTreeNode(int h, Object k) {
// 使用根結點調用find方法
return ((parent != null) ? root() : this).find(h, k, null);
}
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
// 如果一個節點的父爲空那麼他就是 根節點 上面的for是一個類似死循環的東西,只有當r爲null時退出 而r又被每次循環賦值爲r.parent
if ((p = r.parent) == null)
return r;
r = p;
}
}
然後就是 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;
}
接下來看 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;
}
首先來看resize方法動態擴容
final Node<K,V>[] resize() {
// 新建一個 oldTab 指向 table
Node<K,V>[] oldTab = table;
// 獲取舊的 數組大小和擴容閾值
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
// 新建新的 數組大小和閾值
int newCap, newThr = 0;
// 如果舊的 cap大於0
if (oldCap > 0) {
// 舊的大小已經超過 hashMap的最大長度
if (oldCap >= MAXIMUM_CAPACITY) {
// 閾值視爲int最大值
threshold = Integer.MAX_VALUE;
return oldTab;
}
// 如果原來的oldCap * 2 小於最大值,且原來的 大小大於等於 16 那麼新的 閾值 等於舊閾值 * 2
// 這裏如果 oldCap * 2 大於最大值那麼 新閾值就不被賦值
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
// 如果 oldCap == 0 也就是沒有初始化
// newCap = 原先的閾值
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// oldCap 和 oldThr 都爲 0 那麼就設置新的cap 爲 default = 16 newThr = 16 * 0.75
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 如果newThr == 0 也就是 上面 oldCap * 2 超過 最大長度限制
if (newThr == 0) {
//定義一個float
float ft = (float)newCap * loadFactor;
// 如果新的cap或者新的閾值都小於 int最大長度 那麼就能轉爲int 否則 直接設置爲最大值
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 設置類的新閾值
threshold = newThr;
// 接下來就是生成新的table 並且把數據 重新塞到新table中
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
// 如果是第一次初始化 這裏的oldTab是null 直接返回新table即可
if (oldTab != null) {
// 進到這裏說明就table 有信息就循環信息 循環的是數組的頭結點
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
// 這裏e就是頭結點 並且頭結點不爲null
if ((e = oldTab[j]) != null) {
// 原來的數據沒有用了 置爲null等待gc回收
oldTab[j] = null;
// 如果e沒有next 說明該索引位置只有一個節點 直接 算hash桶 塞到新的table的頭結點
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
// 如果節點是紅黑樹
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;
/**
* e.hash & oldCap 判斷用來計算hash值的高位是否爲1。
* 這個1決定着節點在新的數組中的位置,這是因爲數組擴容即 oldCap << 1 右移了一位
* 舉個栗子,假設原數組容量爲 4 (只要是2的冪就行),即: 100
* hash值爲 5,(101) 的節點在原數組的下標爲:
* 5 & (4-1)
* = 101
* & 011
* = 001;
* 在 數組容量爲4的情況下 索引位置爲 1
* 數組擴容後,容量爲8 (1000), 節點在新數組下標爲 :
* 5&(8-1)
* = 101
* & 111
* = 101;
* 在 數組容量爲8的情況下 索引位置爲 5。
* 我們再來看一個例子:
* hash值爲 1,(101) 的節點在原數組的下標爲:
* 1 & (4-1)
* = 001
* & 011
* = 001;
* 在 數組容量爲4的情況下 索引位置爲 1
* 數組擴容後,容量爲8 (1000), 節點在新數組下標爲 :
* 1&(8-1)
* = 001
* & 111
* = 001;
* 在 數組容量爲8的情況下 索引位置爲 1。
* 所以在擴容後 原鏈表的值 由於高位不同 就要分成兩組 ,這時我們直接拿 2的冪 的容量大小 和 hash &
* 就能得到它在擴容2倍後是在原索引位置還是在 原索引位置+ 擴容數位置
* 新的下標正好等於 舊的下標 001 加上 舊容量 4(100) ,即 101.
* 上面是hash & oldCapacity = 1 的情況
* 當 hash & oldCapacity = 0 時自然容易算出新的下標和舊的下標相等。
*/
// 由以上結論可知 e.hash & oldCap爲1時 他就要位移到 原索引位置 + 擴容數的位置
// 爲 0 時還在原位
if ((e.hash & oldCap) == 0) {
// 這裏操作的是lo 也就是還在原索引位置的Node
// 如果 loTail爲null 說明 位置爲空 也就是第一次進來時
// 這時 loHead = e = loTail
// 第二次進來時 走else loHead.next = loTail.next = e
// 然後 loTail = e
// 第三次進來時 走else loHead.next.next = loTail.next = e
if (loTail == null)
loHead = e;
// 否則 當前指向位置的next爲e
else
loTail.next = e;
// 當前指針下移一位
loTail = e;
}
// 這裏同理 只是 操作的是 hi相關
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 沒有next 以後 newTab[j] = head
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 高位 爲1 的 到 新擴容的地方去
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
此方法初看前面比較簡單 但是到後面 賦值 給新 table的時候就會非常繞 主要就是 控制新生產的table 維持新的鏈表結構
重點理解 2的冪次長度 的 hash & oldCap 和 hash & (oldCap -1 ) 的關係、靈活應用了 擴容兩倍後 高位爲1的hash值會跑到新的鏈表裏 也就是[j+oldCap]的索引處。
看完了resize()方法,我們來看 鏈表轉樹的方法 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;
// 這裏其實只是構建了TreeNode 的節點 以及 構建了一個雙向的鏈表 還沒有轉換成樹
do {
// 這裏其實就是簡單的 new 了一個 TreeNode
TreeNode<K,V> p = replacementTreeNode(e, null);
// 第一次tl是null 就給 hd掛上 p
if (tl == null)
hd = p;
// 不是第一次 循環
else {
// p的上一個是pl
p.prev = tl;
// tl的下一個是p
tl.next = p;
}
// 然後tl = p 遊標下移 準備下一次循環
tl = p;
} while ((e = e.next) != null);
// 如果有值 真正的轉爲 紅黑樹纔開始
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
這裏其實就是給原先的Node節點綁上 prev屬性和 next屬性,原先node屬性只有next屬性,這個方法的邏輯很簡單。最主要的還是看下面的treeify方法
這個纔是真正轉樹的地方
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
// 這裏的 this 就是hd
for (TreeNode<K,V> x = this, next; x != null; x = next) {
// 開始遍歷 鏈表
next = (TreeNode<K,V>)x.next;
// 清除 left 和 right (這裏可能是因爲 tree退化會鏈表時沒有清除left 和 right)
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;
// 從 root 開始 找該節點 應該在樹的 位置
for (TreeNode<K,V> p = root;;) {
int dir, ph;
K pk = p.key;
// 如果這次循環的 hash大於 x的hash dir - 1 說明要向左找子
if ((ph = p.hash) > h)
dir = -1;
//反之到右邊找
else if (ph < h)
dir = 1;
// 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);
//知道了dir 繼續向下
// 保留當前位置
TreeNode<K,V> xp = p;
// 當前p 根據dir 的規則 變成了 p的左 或者 右 並且這是左或者右爲null 時 說明找到了 x的正確位置
if ((p = (dir <= 0) ? p.left : p.right) == null) {
// x的父節點就是 xp (這裏p已經變成了 p的左或右 因爲 p.parent 可能爲 null 這裏給x.parent 賦值)
x.parent = xp;
// 根據dir 給 xp 幫左子樹還是右字數
if (dir <= 0)
xp.left = x;
else
xp.right = x;
// 重新構建平衡樹 也就是把x 放到紅黑樹的正確位置
// 進行紅黑樹的插入平衡(通過左旋、右旋和改變節點顏色來保證當前樹符合紅黑樹的要求)
root = balanceInsertion(root, x);
break;
}
}
}
}
// 如果root節點不在table索引位置的頭結點, 則將其調整爲頭結點
moveRootToFront(tab, root);
}
上面的步驟中給每一個TreeNode找到自己合適的位置,也就是balanceInsertion()方法之前的代碼,定位當前TreeNode 屬於左子樹還是右子樹。而balanceInsertion纔是真正保證樹是紅黑樹。
接下來balanceInsertion方法
然後是moveRootToFront方法,將root節點移至頭結點
static <K,V> void moveRootToFront(Node<K,V>[] tab, TreeNode<K,V> root) {
int n;
if (root != null && tab != null && (n = tab.length) > 0) {
// hash 桶
int index = (n - 1) & root.hash;
//得到樹的第一個節點 後面 tab[index] 會被賦值 這裏做保留
TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
// 如果 root 不等於 first
if (root != first) {
Node<K,V> rn;
// 索引值頭結點設置爲root
tab[index] = root;
// 維護 鏈表結構
// 拿到上一個節點
TreeNode<K,V> rp = root.prev;
// 這裏相當於是 rn ->rp rp -> rn 把 root給解放出來 並且掛在first前面
// 由於原來first 前就是null 所有不影響,把root移除也不影響原先的鏈表結構
// rn的上一個是rp
if ((rn = root.next) != null)
((TreeNode<K,V>)rn).prev = rp;
// rp的下一個是rn
if (rp != null)
rp.next = rn;
// first的上一個是 root
if (first != null)
first.prev = root;
// root的下一個是 first
root.next = first;
// root的前一個是null
root.prev = null;
//到這就重新把first加入鏈表
}
assert checkInvariants(root);
}
}
這個過程也是很簡單 就是如果原先頭結點不是root 就把 原來的first 轉爲 root的下一個 然後把root 放到 頭結點。
接下來我們來看 putAll方法
public void putAll(Map<? extends K, ? extends V> m) {
putMapEntries(m, true);
}
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) { // pre-size
// 重新計算需要的數組長度
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
// 如果 大於了閾值 重新計算 閾值
if (t > threshold)
threshold = tableSizeFor(t);
}
// 如果長度大於閾值 直接 擴容
else if (s > threshold)
resize();
// 其實 就是putVal
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
主要的方法就是 putMapEntries 其實他最終調用的其實就是 putVal方法。然後就是一些計算容量 擴容的東西了。
接下來我們來看containsKey方法
// 直接getNode
public boolean containsKey(Object key) {
return getNode(hash(key), key) != null;
}
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 &&
// hash桶 拿到頭結點
(first = tab[(n - 1) & hash]) != null) {
// 如果頭結點就是 目標節點 直接返回 這裏其實利用了 無論是Node還是TreeNode 第一個節點 都是可以直接判斷的
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
// 如果頭結點不是目標 就next
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;
}
// 這裏其實調用的TreeNode的getTreeNode的 這個方法之前已經說過了
final TreeNode<K,V> getTreeNode(int h, Object k) {
return ((parent != null) ? root() : this).find(h, k, null);
}
接着講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;
if ((tab = table) != null && (n = tab.length) > 0 &&
// 又是hash桶 hash桶真是無處不在啊
(p = tab[index = (n - 1) & hash]) != null) {
Node<K,V> node = null, e; K k; V v;
//其實這裏就是getNode 的邏輯 找到需要刪除的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)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
// 如果找到了node
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
// 如果是tree
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
// 如果是頭結點 將頭結點置爲 下一個節點
else if (node == p)
tab[index] = node.next;
// 不是頭結點 就 把p的next 指向node的next 這裏其實p就是node的下一個節點 因爲上面循環找node 的同時 p的指向一直在變
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
就像上面說的,有從鏈表轉爲樹,就有從樹退化爲鏈表removeTreeNode方法是一個極其複雜的過程 我還沒看明白,這裏想說一點 之前我猜測要將left 和 right 置爲null的原因就是因爲退化爲tree的時候 我們的 left 和 right 並未被清空。
final Node<K,V> untreeify(HashMap<K,V> map) {
Node<K,V> hd = null, tl = null;
for (Node<K,V> q = this; q != null; q = q.next) {
Node<K,V> p = map.replacementNode(q, null);
if (tl == null)
hd = p;
else
tl.next = p;
tl = p;
}
return hd;
}
看上去不是很難理解,接下來看一下clear 和 containsValue方法
public void clear() {
Node<K,V>[] tab;
// 操作次數+1
modCount++;
if ((tab = table) != null && size > 0) {
size = 0;
// 循環 給tab置null 並沒有直接 tab = new tab[];
for (int i = 0; i < tab.length; ++i)
tab[i] = null;
}
}
public boolean containsValue(Object value) {
Node<K,V>[] tab; V v;
if ((tab = table) != null && size > 0) {
// 兩重循環 外層 遍歷tab數組 內層遍歷兩邊 這裏並沒有用到紅黑樹的特性
// 所以如果 鏈表很長 會有性能的損耗,說實話我之前並不知道hashMap還有這個方法
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next) {
if ((v = e.value) == value ||
(value != null && value.equals(v)))
return true;
}
}
}
return false;
}
到這裏我們基本的方法就看的差不多了剩下的紅黑樹的構建與維護以及1.8新增的幾個函數式方法 我還沒看明白,後續補充吧。