探索HashMap底層實現
前言
探索HashMap
底層實現是基於JDK1.8
,看程式碼之前翻了一下別人寫的部落格我才知道JDK1.7
版本的HashMap是由陣列 + 連結串列
的資料結構組成,而對於JDK1.8是由陣列 + 連結串列 + 紅黑樹
的資料結構組成,所以我又去了解了什麼是二叉樹、平衡二叉樹、紅黑樹,為的就是能做個鋪墊。既然是由陣列、連結串列、紅黑樹組成,加上平時我們瞭解過的一些細節,可以猜到它的資料結構應該是這樣子的,如圖所示:
長的雖然有點醜。下面我們就開始探索它吧,還是先從註釋開始看!
閱讀註釋
HashMap的每個節點都由一個鍵值對組成,也就是一個Key對應著一個Value,相當於繫結關係了,這兩者都可以為Null。Hashtable的程式碼我還沒有看過,不過這個結論不用質疑。HashMap是無序
簡單來說,迭代所需的時間與遍歷陣列、連結串列、紅黑樹的大小成比例,因為資料有可能存放在連結串列、紅黑樹中,它要把所有的資料都遍歷出來。
當雜湊表中元素的填充數量超過載入因子與當前容量的乘積時,就會發生擴容!
為什麼較高的載入因子會減少空間開銷,增加了查詢的成本呢?載入因子表示元素填滿的程度,在容量不變的情況下,隨著載入因子越大,填滿的元素就越多,空間利用率變大了。假設容量固定值為16,有以下情況:
-
A. 載入因子0.75,該陣列在不擴容的情況下最多可填充0.75 * 16 = 12。
-
B. 載入因子1,該陣列在不擴容的情況下最多可填充1* 16 = 16。
所以我們說空間利用率變大了,同樣的,元素多了,查詢的成本也就自然增加了。
若是知道鍵值對的數量,可建立具有指定容量大小的HashMap。
HashMap屬於非執行緒安全,需要在外部進行控制。
Java提供了方法來保證HashMap的執行緒安全。
該說法很ArrayList很像。
資料結構
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { // 唯一序列號 private static final long serialVersionUID = 362498820763181265L; /** * 預設的初始容量,必須是2的冪次方!!! * * 1 << 4 = 16,為什麼不直接寫16呢? * 我們都知道計算機底層都是用二進位制操作的,而如果直接寫16的話最終還是要轉換成二進位制,對於位運算來說,就是直接用二進位制進行計算的,所以效率會更高一些 * * 為什麼是2的冪次方? * HashMap的底層是由陣列 + 連結串列 + 紅黑樹組成的,那麼先從陣列開始存起,先不管規則如何,想要往陣列中存入元素,必須要先知道指定索引 * 雖然和ArrayList一樣都是陣列,但對於HashMap新增一個元素來說,並不是按照順序存入,那將和ArrayList沒什麼區別了 * 既然不是按順序存入,那就是按照一定的規則去計算索引了,當然了,這個規則不能是死的,所以只能拿著傳入的值進行一定的規則運算 * * 因為HashMap可以存入任意型別的元素,不管是Key還是Value,所以對於任意型別來說都應該可以使用這個規則,也就是說它必須存在於任意型別中,不管是方法還是成員變數,那我們能想到的 * 也只有Object了,它是所有型別的父類,加上索引的值應該是int型別的,所以它最終採用了hashCode方法 * * 不過你會發現hashCode方法返回的數值是不可預測的,而對於HashMap中的陣列來說,索引的取值必須要在0~15之間,所以這個規則還沒有結束,必須把hashCode的結果控制在0-15之間 * 第一個反應想到的對容量大小採用取餘運算,沒錯,是可以的,但是有更好的方法,就像上面一樣,為了提高效率採用了位運算,這裡我們採用的位運算是& * * 那為什麼不用 | 呢?只能說在A | B計算中(A相當於是hashCode,B相當於是容量大小),B沒辦法對結果產生範圍性的控制,比如下面: * * A: 1000 * | * B: 0111 * ---------- * 1111 * B的最大值只能取到7,而結果確實15,肯定不行,所以最終採用了&。 * * 因為索引的最大值只能取到15,所以是 & 15,如果是16,那麼就能取到16造成索引越界了,不過它有一些要求(重點來了),細心的同學會發現在構造HashMap時是可以指定其他值,比如13、17,這些都不是2的冪次方,我們假設採用13 * * 1000 0010 * & & * 1101 1101 * ------- ------- * 0000 0000 * * 你可以多計算幾個,我們發現,有些索引它沒辦法取到,就上面的索引為2它取不到,關鍵就是因為1101(13)中的第三位是0,不管上面的取值(hashCode)如何,它的結果都是0,所以它的值必須要是1才可以,才能取到所有的值,所以應該是1111這樣* 子的一種格式,就是最後幾位都是1,不能是0和1發生間隔,而我們剛才說了最多隻能取到1111(15),這個值是由容量大小 -1 決定的,所以應該是capacity -1的結果要是1111的這種格式才可以,我們發現: * capacity = 111 + 1 = 1000 = 8 * capacity = 1111 + 1 = 10000 = 16 * capacity = 11111 + 1 = 100000 = 32 * capacity = 111111 + 1 = 1000000 = 64 * * 多的就不計算了,這些結果都是2的冪次方,不知道我講明白了沒有... * * * ===========================================================分割線========================================================================================= * * * 既然已經解釋那麼多,還差一個,反正後面還是要解釋的,就一起得了... * 到目前為止我們得到的結論是:hashCode & (capacity - 1) ,還沒有結束... * * 為了更好的說明接下來的問題,我們隨便取值一個hashCode來做運算 * 情況A: * 0000 0010 0100 0110 0000 0110 0000 0011 * & * 0000 0000 0000 0000 0000 0000 0000 1111 * --------------------------------------------- * 0000 0000 0000 0000 0000 0000 0000 0011 * * 看情況A我們可以看出對於hashCode來說,它的後面好幾位(從右向左看)基本上沒啥用,反正不管是1還是0,結果都是0,排面都是靠低位撐著呢...也就是說索引的結果基本上由低位說了算的,所以這樣子造成了一個現象,有可能出現高位不同低位相* 同的兩個數計算出的索引結果是一致的!看情況B: * * 情況B: * 1111 0000 0000 0000 0000 0000 0000 0011 * & * 0000 0000 0000 0000 0000 0000 0000 1111 * ---------------------------------------------- * 0000 0000 0000 0000 0000 0000 0000 0011 * * 情況A與情況B的結果是一致的,可是它們的hashCode是不一樣的啊,說明了這是兩個不同的數,有點問題啊!所以我們必須對這個hashCode在做一次計算,因為要使得高位不同得出的結果應該也要不同,故而要拿這個高位來做文章才能做區分,最簡單 * 的方式是拿hashCode的高位與hashCode自身在做一次計算,在HashMap中16位我們稱之位高位(從左向右開始數),所以是拿這個16位在做一次運算,同樣是採用位運算,有可能是&、|、^(異或) * 上面我們說,有可能出現高位不同低位相同的兩個數,這裡也考慮了高位相同低位不同的情況,針對不同的位運算我們採用假設: * * 1. 若是採用位運算&,AB兩個數的低位都為0,A的高位為1,B的高位為0,結果:A:10 & 01 -> 00 B:00 & 00 -> 00 * 2. 若是採用位運算|,AB兩個數的低位都為1,A的高位為0,B的高位為1,結果:A:01 | 00 -> 01 B:11 & 01 -> 11 雖然看著好像並不會造成問題,我們在看看高位相同低位不同的例子: * 同樣採用位運算|,AB兩個數的高位都為1,A的低位為0,B的低位為1,結果:A:10 | 01 -> 11 B:11 & 01 -> 11 這裡就出現了計算結果一致了 * * 3. 若是採用位運算^,AB兩個數的低位都為1,A的高位為0,B的高位為1,結果:A:01 ^ 00 -> 01 B:11 ^ 01 -> 10 * 同樣採用位運算^,AB兩個數的低位都為0,A的高位為1,B的高位為0,結果:A:10 ^ 01 -> 11 B:00 ^ 00 -> 00 * 同樣採用位運算^,AB兩個數的高位都為1,A的低位為1,B的低位為0,結果:A:11 ^ 01 -> 10 B:10 ^ 01 -> 11 (比較高位相同低位不同) * 同樣採用位運算^,AB兩個數的高位都為0,A的低位為0,B的低位為1,結果:A:00 ^ 00 -> 00 B:01 ^ 00 -> 01 * 已經很明顯了,最終採用的位運算是^,所以會經過如下計算:hashCode ^ (hashCode >>> 16) * * * ==============================================================分割線==================================================================================== * * * 上面說的有點多來總結下吧: * 1. 為什麼HashMap的容量大小必須是2的冪次方:因為在HashMap中採用了位運算&代替了取餘運算%,這樣做是為了提升效率,不過在替換運算子號的同時也有一個要求,為了能取到區間中的每個數, * 比如0-15中的每個數都能取到,也就是說最後幾位要是連續的1(01111,從左向右看),不能是0與1發生間隔,而把這個數去 + 1就等於容量大小,你會發現它正是2的冪次方,所以按照我個人的理解來說,就是在將&替換了%後,為了能取到區間中的每* 個索引,必須是2的冪次方,有人將這種做法叫做均勻分佈 * * 2. 為什麼採用hashCode方法:HashMap能存放任意型別的資料,要想按照HashMap的規則進行運算,在任意型別的資料當中一定存在著這些規則,那就只有Object#hashCode * * 3. result = hasCode ^ (hashCode >>> 16)目的是什麼:為了降低不同的數導致同樣的結果(索引),簡單來說就是為了降低碰撞,但還是有可能發生碰撞 * * 4. result & (capacity - 1):計算索引的位置 */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; /** * 最大容量值,必須是2的冪次方 * 當通過建構函式傳入更高的值時會使用最大容量指來代替 */ static final int MAXIMUM_CAPACITY = 1 << 30; /** * 預設的載入因子 * 設定成0.75是對空間與時間的一個權衡(折中),載入因子過大會減少空間開銷,增加查詢成本 */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** * 當新增節點後連結串列的長度超過該數值時會將連結串列轉換為紅黑樹,提升查詢速度,但同時記憶體使用會增大,因為樹節點的大小約是常規節點的兩倍 * * 為什麼是8? * 在節點良好分佈的情況下,基本不會用到紅黑樹。而在理想情況下的隨機雜湊,節點分佈遵循泊松分佈,連結串列下的長度達到8已經是非常小的概率,超過8的概率我們認為是幾乎不可能發生的事情 * 不過HashMap還是做了預防措施,當連結串列的長度達到8時會被轉換成紅黑樹,至於為什麼不是7,個人認為8更合適,應該儘可能的提升效能. */ static final int TREEIFY_THRESHOLD = 8; /** * 當紅黑樹的節點個數小於該數值時,紅黑樹將轉換回連結串列 * 這裡有個點很重要,當初我以為紅黑樹在刪除節點後長度就會變小,那應該會按照這個指標來將其變成單向連結串列結構,可惜不是,紅黑樹在刪除節點前會判斷是否此樹過小,若過小則轉換為連結串列,若不是則刪除節點並進行自我平衡,所以只有在重新雜湊時* 才會判斷該數值!!!! * * 為什麼不是7? * 若是頻繁地新增刪除新增刪除元素,那麼HashMap將在轉換中消耗很大的效能,而7的空隙讓它有一個很好的緩衝 */ static final int UNTREEIFY_THRESHOLD = 6; /** * 當連結串列的長度大於8時: * 若雜湊表的容量大於64,則將連結串列轉換成紅黑樹 * 若雜湊表的容量小於64,資料結構保持不變,對雜湊表進行擴容,擴容時原來的節點可能在舊的索引上,有可能在新的索引上(原來的索引 + 舊的容量大小) * 至少應該是4 * TREEIFY_THRESHOLD,防止重新雜湊和樹化閾值(TREEIFY_THRESHOLD)產生衝突,因為如果連結串列的長度剛好達到8,這個時候轉成紅黑樹,而如果又剛好發生擴容,那麼此顆紅黑樹又將可能被拆分成連結串列 * 所以一開始的紅黑樹轉化有可能相當於白做了,所以又加上了陣列容量為64的限定條件,只能說32比16更適合作為一個限定條件 * 在雜湊表容量很小的情況下,隨著不斷的新增節點,連結串列的長度會越來越大,也會有越來越多的連結串列,當長度超過一定的閾值之後便需要轉換成紅黑樹,而在擴容時又需要拆解成連結串列,這些都是需要一定的成本,所以在容量較小的情況下直接選擇擴容 */ static final int MIN_TREEIFY_CAPACITY = 64; /** * 自定義載入因子 * 容量大小不變的情況下,載入因子過大減少空間開銷,增加查詢成本 * * 注意:該屬性是可以指定大於1,但是會造成一定的成本,具體可看threshold屬性說明,最終是建議不應該超過1 */ final float loadFactor; /** * 雜湊表 * 陣列結構中包括連結串列、紅黑樹 */ transient Node<K,V>[] table; /** * 快取entrySet方法的返回值 */ transient Set<Map.Entry<K,V>> entrySet; /** * 鍵值對的數量 */ transient int size; /** * HashMap中結構修改的次數 * 在上面的翻譯中我們已經解釋了什麼是結構修改 * 該成員屬性是用於檢測迭代器的快速失敗 */ transient int modCount; /** * 擴容前需要判斷的閾值 * 若超過該值則擴容,若沒超過則不需要 * 該值的計算方式:capacity * load factor * * 注意:該屬性是可以超過指定容量大小(capacity),準備來說,應該是載入因子(load factor)可以指定大於1,下面假設是2 * 相當於指定了容量大小是10,但是會到大於20時才會擴充容量 * 當填充的元素個數超過10個而小於20個後,那麼後續的元素必定會造成碰撞從而形成連結串列或紅黑樹,這就為後續的增加/查詢造成了一定的成本 * 所以建議載入因子(load factor)不應該超過1 * * 我為什麼會想到這個注意點呢,是因為putMapEntries方法中有一段程式碼:float ft = ((float)s / loadFactor) + 1.0F,這段程式碼很有意思,可以看具體方法中的說明 */ int threshold; /** * 儲存Key */ transient Set<K> keySet; /** * 儲存Value */ transient Collection<V> values; }
HashMap的大部分知識點,包括一些細節方面,其實都在上面的註釋中提到了,應該儘可能的去理解它!
建構函式
/**
* 指定初始容量與載入因子構造雜湊表
* 在上面中我們提到了容量必須是2的冪次方,所以呼叫tableSizeFor方法來進行調整
* Float.isNaN:檢測是否是數字
* @param initialCapacity 指定初始容量
* @param loadFactor 指定載入因子
*/
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);
}
/**
* 指定初始容量與預設載入因子(0.75)構造雜湊表
* @param initialCapacity 指定初始容量
*/
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
/**
* 預設初始容量(16)與預設載入因子(0.75)構造雜湊表
*/
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
/**
* 將指定集合新增到雜湊表中,採用預設載入因子
* @param m 指定集合
*/
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
方法說明
簡單方法
/**
* 將指定容量大小調整到2的冪次方
* 具體是調整到比該容量還大的最近2的冪次方
* cap = 21 最終調整到 32
* cap = 15 最終調整到 16
* 若是2的冪次方則結果是原來的值
* @param cap 指定容量大小
* @return 2的冪次方
*/
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;
}
/**
* 將指定集合中的元素新增到雜湊表中
* 若雜湊表為空,表示式s/loadFactor有可能出現帶有小數的情況,比如1.6、1.4這樣子的一種格式
* 而後續的 + 1.0F 和 (int)ft 相當於是對小數點做一個向上取整,以儘可能的保證更大容量
* 若雜湊表不為空,則預先進行擴容一次,若沒有預先進行擴容,而是等到後續新增元素達到閾值後才開始擴容,那個時候隨著元素的增加擴容所消耗的時間也會增加,簡單來說,減少了一定的時間成本
*
* 總結:
* 若雜湊表已初始化,則採用它自身的容量進行擴容
* 若雜湊表未初始化,則採用集合的容量作為雜湊表的容量大小,前提是大於雜湊表的容量大小
* @param m 指定集合
* @param evict
*/
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();
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);
}
}
}
/**
* 獲取雜湊表中的元素個數
* @return 雜湊表中的元素個數
*/
public int size() {
return size;
}
/**
* 判斷雜湊表是否為空
* @return 雜湊表是否為空
*/
public boolean isEmpty() {
return size == 0;
}
/**
* 根據指定鍵查詢節點
* HashMap在插入節點時先通過hashCode進行比較,若兩者相同在通過equals進行比較,若也相同則認為是重複,會進行相應的覆蓋,若不相等則用連結串列或紅黑樹進行儲存
* 按照如此的思路,要查詢某個節點,則hashCode與equals必須都相等,若有其中一個不相等則繼續查詢下一個節點
* @param hash 鍵的雜湊值
* @param key 鍵
* @return 指定鍵對應的節點
*/
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;
}
/**
* 根據指定鍵計算雜湊值
* 至於為什麼是這麼計算的可參考一開始提到的
* @param key 鍵
* @return 雜湊值
*/
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
/**
* 判斷指定類是否實現了Comparable,同時Comparable的泛型是它自身
* 簡單來說就是判斷指定類C是否滿足`class C implements Comparable<C>`這樣子的一種格式,若滿足則返回該類的class,否則返回null
* @param x 指定類
* @return x.getClass or 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與kc的型別相同則比較x與k的大小,實際上k與kc的類型別是同一個,簡單來說就是在比較k與x的大小,既然是比較,類型別是一樣的才有意義
* @param kc k的類型別
* @param k 比較值
* @param x 另一個比較值
* @return 比較結果
*/
static int compareComparables(Class<?> kc, Object k, Object x) {
return (x == null || x.getClass() != kc ? 0 :
((Comparable)k).compareTo(x));
}
/**
* 判斷是否包含指定鍵
* @param key 指定鍵
* @return 是否包含指定鍵
*/
public boolean containsKey(Object key) {
return getNode(hash(key), key) != null;
}
/**
* 初始化雜湊表或以兩倍的大小進行擴容,擴容後進行重新雜湊-rehash
*
* rehash機制:
* 若指定索引上存在元素且沒有連結串列或紅黑樹結構,則在新陣列上重新計算索引並填充即可
* 若指定索引上存在元素且結構是連結串列,將連結串列分成高低位兩條連結串列並往新陣列中填充,高位連結串列會儲存在新索引上(原索引 + 舊容量大小),低位連結串列儲存在原索引上
* 若指定索引上存在元素且結構是紅黑樹,將紅黑樹分成高低位兩棵樹,低位這棵樹中的長度若超過8,在判斷高位那棵樹是否存在,若不存在說明低位已經樹形化過,若存在說明結構已經修改,低位需要重新樹形化,若低位這棵樹的長度不超過8則將這棵樹轉換* 成單向連結串列的結構,同理對於處理高位這棵樹也是如此判斷
* @return 新陣列
*/
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) {
if (oldCap >= MAXIMUM_CAPACITY) { //舊容量超過最大容量時
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY) //新容量是舊容量的兩倍大小
newThr = oldThr << 1; //新閾值是舊閾值的兩倍大小
}
else if (oldThr > 0) //threshold大於0說明採用了自定義的初始容量大小,而一開始threshold存放了初始容量的大小
newCap = oldThr;
else { //threshold小於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) {
oldTab[j] = null;
if (e.next == null) //雜湊表指定索引上只有一個元素,也就是說該位置上不存在連結串列或者紅黑樹之類的資料結構
newTab[e.hash & (newCap - 1)] = e; //將該位置上的元素按照新容量重新雜湊,也就是在新陣列中重新計算索引並填充,至於為什麼是這麼計算的,最上面應該提到了
else if (e instanceof TreeNode) //雜湊表指定索引上是紅黑樹,紅黑樹將自身拆解成高低位兩棵樹,具體可參見split方法
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else {
/**
* 雜湊表指定索引上是一條連結串列,根據雜湊值將一條連結串列上的節點分成高位、低位兩部分組成的兩條連結串列(分組),將高低位兩條鏈條填充到新陣列中,高位填充到新索引(原索引 + 舊容量大小)處,而低位的索引保持不變
*/
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
/**
* 首先應該明白oldCap是2的冪次方,它的二進位制應該是0000 0000 0000 0000 0000 0000 0000 1000 這樣子的一種格式
* e.hash & oldCap:將結果大於0的節點構成一組連結串列,既然是連結串列就有頭部節點與尾部節點,所以這就對應了上面的hiHead、hiTail,我們稱作高位;同樣的將結果小於0
* 的節點構成另外一組連結串列,對應著loHead、loTail,我們稱作低位
*/
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);
/**
* 為什麼在新的雜湊表中索引只可能出現在舊索引處或舊索引 + 舊容量大小處
* 首先索引的計算方式是:e.hash & (capacity-1),方便理解我們直接舉例
* oldCapacity:16 newCapacity:32 node1:0000 0000 0000 0000 0000 1111 0000 1010 node2:0000 0000 0000 0000 0000 1111 0001 1010
* node1 & (oldCapacity -1) node2 & (oldCapacity -1)
*
* 0000 0000 0000 0000 0000 1111 0000 1010 0000 0000 0000 0000 0000 1111 0001 1010
* & 0000 0000 0000 0000 0000 0000 0000 1111 & 0000 0000 0000 0000 0000 0000 0000 1111
* -------------------------------------------- ------------------------------------------
* 0000 0000 0000 0000 0000 0000 0000 1010 0000 0000 0000 0000 0000 0000 0000 1010
*
* node1 & (newCapacity -1) node2 & (newCapacity -1)
*
* 0000 0000 0000 0000 0000 1111 0000 1010 0000 0000 0000 0000 0000 1111 0001 1010
* & 0000 0000 0000 0000 0000 0000 0001 1111 & 0000 0000 0000 0000 0000 0000 0001 1111
* -------------------------------------------- ------------------------------------------
* 0000 0000 0000 0000 0000 0000 0000 1010 0000 0000 0000 0000 0000 0000 0001 1010
*
* 在舊容量當中索引取決於最後的4位,而在新容量當中索引取決於最後的5位,舊容量與新容量相比最大的差別在於倒數第5位上,而能造成它們索引不同的情況就要看hash值的倒數第5位上是否是1
* 若不是1,&完之後結果自然為0,此時的新容量下的索引與舊容量下的索引是一致的;若是1,&完之後結果自然是1,而正好該位置上是舊容量的大小(倒數第5位),如果轉換成十進位制的話,此時新容量下的索引是舊容量下的索引 + * 舊容量大小,所以我們可以得出在新容量下的索引只可能出現在這兩種情況下。而在計算當中採用 e.hash & oldCap就是在判斷倒數第5位是否是1,若是1則索引是舊容量下的索引 + 舊容量大小,若不是1,則還是原來的索引
*/
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
/**
* 將指定索引上的單向連結串列結構轉成紅黑樹 + 雙向連結串列
* 連結串列長度不僅要超過8,同時雜湊表的長度要超過64,否則直接進行擴容
* 為什麼還要採用雙向連結串列呢?
* 因為在轉成紅黑樹後需要將根節點移動到連結串列的頭部,這就需要更改連結串列節點之間的關係,而單向連結串列中是不知道上一個節點是誰,若想知道的話就還需要從頭遍歷,所以採用雙向連結串列
*
* @param tab 新陣列
* @param hash 雜湊值
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
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)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab); //將雙向連結串列轉成紅黑樹
}
}
/**
* 將節點資訊轉換成紅黑樹節點資訊
* @param p 節點資訊
* @param next 下一個節點資訊
* @return 紅黑樹節點
*/
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}
/**
* 轉換成普通的節點資訊
* @param p 節點資訊
* @param next 下一個節點資訊
* @return 普通節點
*/
Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
return new Node<>(p.hash, p.key, p.value, next);
}
/**
* 連結串列樹形化
* 單向連結串列 -> 雙向連結串列 + 紅黑樹
* @param tab 雜湊表
*/
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;
for (TreeNode<K,V> p = root;;) { //存在根節點則要進行左右子樹的比較來選擇最終的儲存點
int dir, ph;
K pk = p.key;
if ((ph = p.hash) > h) //通過比較hash值,此時說明x節點小於根節點,故應該在左子樹
dir = -1;
else if (ph < h) //右子樹上
dir = 1;
else if ((kc == null && //若當前節點與另外一個節點的hash相等的話,就比較鍵值,若鍵值也相同的話就比較類名
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
TreeNode<K,V> xp = p; //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); //將根節點移動到連結串列頭部
}
/**
* 先比較兩個物件的類名是否相等,若相等則比較兩個物件的雜湊值大小並返回結果,若不相等則返回結果
* 若物件為空,則雜湊值等於0
* @param a 物件
* @param b 另一個物件
* @return 比較結果值
*/
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;
}
/**
* 紅黑樹中插入節點後維持平衡
* @param root 根節點
* @param x 插入節點
* @return 根節點
*/
static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root, TreeNode<K,V> x) {
/**
* xp = x.parent x的父節點
* xpp = x.parent.parent x的爺爺節點
* xppl = x.parent.parent.left x的爺爺節點的左子樹
* xppr = x.parent.parent.right x的爺爺節點的右子樹
*/
x.red = true;
for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {
if ((xp = x.parent) == null) { //1 x.parent == null 說明是根節點,將其變成黑色即可
x.red = false;
return x;
}
else if (!xp.red || (xpp = xp.parent) == null) //2 xp = x.parent 說明x的父節點是黑色,不需要任何的操作
return root;
if (xp == (xppl = xpp.left)) { //3 x的父節點在x的爺爺節點的左子樹上
if ((xppr = xpp.right) != null && xppr.red) { //4 x的父節點是紅色,叔叔節點也是紅色
xppr.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
if (x == xp.right) { //5 X的父節點在X的爺爺節點的左子樹上,X在X的父節點的右子樹上
root = rotateLeft(root, x = xp);//注意此時的x變成了原來x的父節點
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) { //6 X的父節點在X的爺爺節點的左子樹上,X在X的父節點的左子樹上
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateRight(root, xpp);
}
}
}
}
else { //7 X的父節點在X的爺爺節點的右子樹上
if (xppl != null && xppl.red) { //8 x的父節點是紅色,叔叔節點也是紅色
xppl.red = false;
xp.red = false;
xpp.red = true;
x = xpp;
}
else {
if (x == xp.left) { //9 x的父節點在X的爺爺節點的右子樹上,X在X的父節點的左子樹上
root = rotateRight(root, x = xp);
xpp = (xp = x.parent) == null ? null : xp.parent;
}
if (xp != null) { //10 x的父節點在X的爺爺節點的右子樹上,X在X的父節點的右子樹上
xp.red = false;
if (xpp != null) {
xpp.red = true;
root = rotateLeft(root, xpp);
}
}
}
}
}
}
/**
* 紅黑樹中指定節點進行左旋
* 過多的演算法就不討論了,可以參考紅黑樹、AVL樹的文章
* @param root 根節點
* @param p 指定節點
* @return 根節點
*/
static <K,V> TreeNode<K,V> rotateLeft(TreeNode<K,V> root,
TreeNode<K,V> p) {
/**
* r = p.right 當前節點的右子樹
* rl = r.left 當前節點的右子樹的左子樹
*/
TreeNode<K,V> r, pp, rl;
if (p != null && (r = p.right) != null) {
if ((rl = p.right = r.left) != null)
rl.parent = p;
if ((pp = r.parent = p.parent) == null)
(root = r).red = false;
else if (pp.left == p)
pp.left = r;
else
pp.right = r;
r.left = p;
p.parent = r;
}
return root;
}
/**
* 紅黑樹中指定節點進行右旋
* 過多的演算法就不討論了,可以參考紅黑樹、AVL樹的文章
* @param root 根節點
* @param p 指定節點
* @return 根節點
*/
static <K,V> TreeNode<K,V> rotateRight(TreeNode<K,V> root,
TreeNode<K,V> p) {
TreeNode<K,V> l, pp, lr;
if (p != null && (l = p.left) != null) {
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;
}
/**
* 將根節點移動到連結串列的頭部
* 看到這裡我發現,它不僅保留了雙向連結串列同時增加了紅黑樹,所以在查詢的時候應該是使用的紅黑樹結構
* @param tab 雜湊表
* @param 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) {
int index = (n - 1) & root.hash;
TreeNode<K,V> first = (TreeNode<K,V>)tab[index];
if (root != first) {
Node<K,V> rn;
tab[index] = root;
TreeNode<K,V> rp = root.prev;
if ((rn = root.next) != null)
((TreeNode<K,V>)rn).prev = rp;
if (rp != null)
rp.next = rn;
if (first != null)
first.prev = root;
root.next = first;
root.prev = null;
}
assert checkInvariants(root);
}
}
/**
* 當前節點作為查詢的起始節點,查詢其所有子節點中是否有匹配到指定節點,匹配必須是hash相同、equals相等
* @param h 雜湊值
* @param k 指定節點
* @param kc 比較器的泛型
*/
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) //當前節點的hash大於指定節點的hash,後續應該從左子樹進行下一輪比較
p = pl;
else if (ph < h) //當前節點的hash小於指定節點的hash,後續應該從右子樹進行下一輪比較
p = pr;
else if ((pk = p.key) == k || (k != null && k.equals(pk))) //當前節點的hash與指定節點的hash相同,同時equals也相等
return p;
//執行到這裡說明當前節點的hash與指定節點的hash相同,但是equals不相等,因為接下來不知道該從左子樹還是右子樹開始查詢,所以要先判斷是否存在左右子樹
else if (pl == null) //若左子樹為空,那隻能從右子樹開始進行下一輪比較
p = pr;
else if (pr == null) // 若右子樹為空,那隻能從左子樹開始進行下一輪比較
p = pl;
else if ((kc != null ||
(kc = comparableClassFor(k)) != null) &&
(dir = compareComparables(kc, k, pk)) != 0)
/**
* 若左右子樹都不為空,那就必須定義一種規則了,通過comparable方法來比較key的大小
* 首先得先檢測是否實現了comparable才可以進行比較-comparableClassFor
* 接著還得檢測是否型別相同,否則也不具備可比性
* 若滿足以上兩點則返回比較結果
*/
p = (dir < 0) ? pl : pr;
else if ((q = pr.find(h, k, kc)) != null) //執行到這裡說明無法通過comparable進行比較或者key在比較之後還是相等,只能先嚐試從右子樹開始查詢
return q;
else //若右子樹遞迴後還是未找到,那麼就從左子樹開始查詢
p = pl;
} while (p != null);
return null; //最後未匹配的話就返回null
}
/**
* 將紅黑樹拆解成高低的兩棵樹,並根據判斷來選擇是轉換成單向連結串列還是重新樹形化
* 若低位這棵樹的長度不超過6,則將其轉換成單向連結串列,同理高位這棵樹也是這麼判斷的
* 若低位這棵樹的長度超過6,且另外一棵樹高位不存在,說明紅黑樹並未將其拆解成兩棵樹,低位這棵樹的結構仍是不變,故而不需要進行重新樹形化,而若高位這棵樹存在,則需要重新樹形化,因為結構已經發生變化
* 同理,高位也是如此判斷
* @param map 物件
* @param tab 新的雜湊表
* @param index 索引
* @param bit 舊的雜湊表長度
*/
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;
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) {
if (lc <= UNTREEIFY_THRESHOLD) //低位這棵樹的長度小於連結串列的閾值則轉換成連結串列結構
tab[index] = loHead.untreeify(map);
else {
tab[index] = loHead;
if (hiHead != null) //若hiHead為空,說明紅黑樹並未拆解成高低位兩棵樹,低位已經被樹形化過了不需要重新樹形化,若hiHead不為空,說明紅黑樹已經拆解成高低位兩棵樹,結構已經發生變化,低位需要重新樹形化
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);
}
}
}
/**
* 紅黑樹轉換單向連結串列
* @param map 物件
* @return 連結串列的頭部節點
*/
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;
}
/**
* 在雜湊表中通過指定鍵移除節點
* 返回值是null表示不存在移除的節點
* @param hash 雜湊值
* @param key 指定鍵
* @param value 指定value,通過hash與equals找到節點後還要根據matchValue來判斷是否需要判斷value值相等
* @param matchValue 標識是否需要判斷value是否相等
* @param movable 標識移除節點後是否需要移動其他節點
* @return 移除的節點或null
*/
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 &&
(p = tab[index = (n - 1) & hash]) != null) {
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 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; // 把當前節點p指向e,這一步是讓p儲存的永遠下一次迴圈裡e的父節點,如果下一次e匹配上了,那麼p就是node的父節點
} while ((e = e.next) != null);
}
}
if (node != null && (!matchValue || (v = node.value) == value || //matchValue標識在找到節點後是否需要判斷value值是否相等
(value != null && value.equals(v)))) {
if (node instanceof TreeNode) //從紅黑樹中移除節點
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p) //說明頭部節點剛好是要移除的節點,直接將它的下一個節點填充到雜湊表中
tab[index] = node.next;
else // 說明連結串列中找到移除的節點,p是node的父節點,由於要刪除node,所有隻需要把p的下一個節點指向到node的下一個節點即可把node從連結串列中刪除了
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
/**
* 在紅黑樹中通過指定鍵查詢節點
* 從根節點開始查詢
* @param h 雜湊值
* @param k 指定鍵
* @return null或節點
*/
final TreeNode<K,V> getTreeNode(int h, Object k) {
return ((parent != null) ? root() : this).find(h, k, null);
}
/**
* 紅黑樹中移除當前節點
* 呼叫該方法前當前節點必須存在
* 作者說該方法的程式碼比典型的紅黑刪除程式碼更為混亂,因為紅黑樹在刪除中會通過交換值的方式來刪除,而在紅黑樹中還維護了一個雙向連結串列,若是直接通過交換值會對雙向連結串列中節點之間的關係造成錯誤,因此它採用了另外一種方式-交換樹連結
*
* 交換樹連結:當前節點與當前節點的右子樹的最左節點進行交換,包括parent、left、right節點的關係都發生交換
* @param map 物件
* @param tab 新的雜湊表
* @param movable 標識移除節點後是否需要移動其他節點
*/
final void removeTreeNode(HashMap<K,V> map, Node<K,V>[] tab, boolean movable) {
//開始處理連結串列
int n;
if (tab == null || (n = tab.length) == 0)
return;
int index = (n - 1) & hash;
TreeNode<K,V> first = (TreeNode<K,V>)tab[index], root = first, rl; //first、root:根節點
TreeNode<K,V> succ = (TreeNode<K,V>)next, pred = prev; //succ:當前節點的下一個節點 pred:當前節點的上一個節點
if (pred == null) //說明當前節點是根節點,直接將它的下一個節點作為根節點
tab[index] = first = succ;
else //說明當前節點是某一個節點,移除當前節點時更改當前節點的上下節點的關係
pred.next = succ;
if (succ != null)
succ.prev = pred;
if (first == null) //走到這裡說明當前節點的下一個節點為空,即表明已經沒有節點了
return;
if (root.parent != null)
/**
* moveRootToFront是用來維持紅黑樹的根節點即為連結串列的頭部,但是在該方法在呼叫moveRootToFront時會進行判斷,也就是它可能不會被呼叫到,那麼會造成紅黑的根節點與連結串列的頭部節點不一致
* 這裡獲取的root指向的是連結串列的頭部節點,並不是紅黑樹的根節點,故而還要往上查詢根節點
*/
root = root.root();
if (root == null || root.right == null ||
(rl = root.left) == null || rl.left == null) {
/**
* 通過root節點來判斷此紅黑樹是否太小, 如果是則呼叫untreeify方法轉為連結串列節點並返回,轉連結串列後就無需再進行下面的紅黑樹處理,這比維持紅黑樹的平衡來說簡單多了
* 因為在刪除節點後紅黑樹需要維持平衡,有可能根節點會發生變化,也有可能被置空(根節點與刪除節點之間是父子節點的關係導致),而又由於沒有呼叫moveRootToFront來更新根節點,導致下次在獲取根節點時可能獲取到的為空
*/
tab[index] = first.untreeify(map); // too small
return;
}
//結束處理連結串列
//開始處理紅黑樹
TreeNode<K,V> p = this, pl = left, pr = right, replacement; //p:當前節點 pl:當前節點的左子樹 pr:當前節點的右子樹
//1. 當前節點有兩個子節點的情況下
if (pl != null && pr != null) {
TreeNode<K,V> s = pr, sl;
while ((sl = s.left) != null) //找到當前節點的右子樹的最左節點,即為s
s = sl;
boolean c = s.red; s.red = p.red; p.red = c; //交換s與p的顏色,也就是交換當前節點與最左節點的顏色
TreeNode<K,V> sr = s.right; //最左節點的右子樹,即為sr
TreeNode<K,V> pp = p.parent; //當前節點的父節點,即為pp
//第一次調整和第二次調整:將當前節點和最左節點進行了位置調換
//第一次調整
if (s == pr) { //如果當前節點的右節點即為最左節點,則將當前節點的父節點賦值為最左節點,將最左節點的右節點賦值為當前節點
p.parent = s;
s.right = p;
}
else {
TreeNode<K,V> sp = s.parent; //最左節點的父節點,即為sp
if ((p.parent = sp) != null) { //將當前節點的父節點變成最左節點的父節點
if (s == sp.left)
sp.left = p; //最左節點應該是在左子樹上,這邊的程式碼應該是會走進去,而不會走到else中;將最左節點的父節點的左子樹變成當前節點
else
sp.right = p;
}
if ((s.right = pr) != null) //最左節點的左子樹必定為null,但有可能還有右子樹,故而將它的右子樹變成當前節點的右子樹
pr.parent = s;
}
//第二次調整
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為最左節點的右子樹
replacement = sr;
else
replacement = p; //若最左節點的右子樹為空,則賦值replacement為當前節點
}
//2. 當前節點只有一個子節點且是左子樹,replacement賦值當前節點的左子樹
else if (pl != null)
replacement = pl;
//3. 當前節點只有一個子節點且是右子樹,replacement賦值為當前節點的右子樹
else if (pr != null)
replacement = pr;
//4. 當前節點無子節點,即本身是個葉子節點,replacement賦值為當前節點
else
replacement = p;
//第三次調整:使用replacement節點替換掉p節點的位置,將p節點移除
if (replacement != p) { //這裡說明當前節點不是葉子節點,直接用replacement代替當前節點
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; //當前節點已經被完整的替換為replacement, 將當前節點清空
}
//若當前節點的顏色是紅色,可以直接刪除,因為刪除一個紅色並不會影響紅黑樹的平衡,否則需要進行紅黑樹的平衡調整,因為刪除黑色會導致黑色數目不一致
TreeNode<K,V> r = p.red ? root : balanceDeletion(root, replacement);
if (replacement == p) { //說明當前節點是葉子節點,也就是說replacement是需要刪除的節點,直接將replacement的關係清空即可
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;
}
}
if (movable) //判斷是否將紅黑樹的根節點移動到連結串列的頭部
moveRootToFront(tab, r);
}
/**
* 調整紅黑樹的平衡
* 該方法可以說是HashMap中最複雜的部分了,不過我認為它是跟演算法有些關係,對於原始碼中的演算法個人認為只要你清楚它在做什麼即可,畢竟你的演算法基本並不怎樣,所以這裡就不做深入研究了
* @param root 根節點
* @param x 當前節點
*/
static <K,V> TreeNode<K,V> balanceDeletion(TreeNode<K,V> root, TreeNode<K,V> x) {
/**
* x:當前節點
* xp:當前節點的父節點
* xpl:當前節點的父節點的左子樹
* xpr:當前節點的父節點的右子樹
*/
for (TreeNode<K,V> xp, xpl, xpr;;) {
if (x == null || x == root) //當前節點是根節點的情況
return root;
else if ((xp = x.parent) == null) { //當前節點是根節點,染成黑色,直接返回,因為調整過後,root並不一定指向刪除操作過後的根節點,如果之前刪除的是root節點,則x將成為新的根節點)
x.red = false;
return x;
}
else if (x.red) { //如果x為紅色,則無需調整
x.red = false;
return root;
}
else if ((xpl = xp.left) == x) { //當前節點為其父節點的左孩子
if ((xpr = xp.right) != null && xpr.red) { //如果它有紅色的兄弟節點xpr,那麼它的父親節點xp一定是黑色節點
xpr.red = false;
xp.red = true;
root = rotateLeft(root, xp);
xpr = (xp = x.parent) == null ? null : xp.right;
}
//如果xpr為空,則向上繼續調整,將x的父節點xp作為新的x繼續迴圈
if (xpr == null)
x = xp;
else {
TreeNode<K,V> sl = xpr.left, sr = xpr.right;
if ((sr == null || !sr.red) &&
(sl == null || !sl.red)) { //若sl和sr都為黑色或者不存在,即xpr沒有紅色孩子,則將xpr染紅
xpr.red = true;
x = xp;
}
else {
if (sr == null || !sr.red) {
if (sl != null)
sl.red = false;
xpr.red = true;
root = rotateRight(root, xpr);
xpr = (xp = x.parent) == null ?
null : xp.right;
}
if (xpr != null) {
xpr.red = (xp == null) ? false : xp.red;
if ((sr = xpr.right) != null)
sr.red = false;
}
if (xp != null) {
xp.red = false;
root = rotateLeft(root, xp);
}
x = root;
}
}
}
else { // symmetric
if (xpl != null && xpl.red) {
xpl.red = false;
xp.red = true;
root = rotateRight(root, xp);
xpl = (xp = x.parent) == null ? null : xp.left;
}
if (xpl == null)
x = xp;
else {
TreeNode<K,V> sl = xpl.left, sr = xpl.right;
if ((sl == null || !sl.red) &&
(sr == null || !sr.red)) {
xpl.red = true;
x = xp;
}
else {
if (sl == null || !sl.red) {
if (sr != null)
sr.red = false;
xpl.red = true;
root = rotateLeft(root, xpl);
xpl = (xp = x.parent) == null ?
null : xp.left;
}
if (xpl != null) {
xpl.red = (xp == null) ? false : xp.red;
if ((sl = xpl.left) != null)
sl.red = false;
}
if (xp != null) {
xp.red = false;
root = rotateRight(root, xp);
}
x = root;
}
}
}
}
}
/**
* 清空雜湊表
*/
public void clear() {
Node<K,V>[] tab;
modCount++;
if ((tab = table) != null && size > 0) {
size = 0;
for (int i = 0; i < tab.length; ++i)
tab[i] = null;
}
}
/**
* 雜湊表中是否包含指定值
* @param value 指定值
* @return 是否包含指定值
*/
public boolean containsValue(Object value) {
Node<K,V>[] tab; V v;
if ((tab = table) != null && size > 0) {
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;
}
/**
* 獲取包含所有鍵的集合
* @return 包含所有鍵的Set集合
*/
public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
ks = new KeySet();
keySet = ks;
}
return ks;
}
/**
* 包含雜湊表中所有鍵的集合
*/
final class KeySet extends AbstractSet<K> {
/**
* 獲取雜湊表的大小,共儲存了多少節點
* @return 雜湊表的大小
*/
public final int size() { return size; }
/**
* 清空雜湊表
*/
public final void clear() { HashMap.this.clear(); }
/**
* 獲取包含所有鍵的迭代器
* @return 包含所有鍵的迭代器
*/
public final Iterator<K> iterator() { return new KeyIterator(); }
/**
* 雜湊表中是否包含指定鍵
* @param o 指定鍵
* @return 是否包含指定鍵
*/
public final boolean contains(Object o) { return containsKey(o); }
/**
* 根據指定鍵移除節點
* @param key 指定鍵
* @return 移除的節點
*/
public final boolean remove(Object key) {
return removeNode(hash(key), key, null, false, true) != null;
}
/**
* 獲取分割迭代器
*/
public final Spliterator<K> spliterator() {
return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
}
/**
* 遍歷所有鍵並執行指定動作
* 遍歷過程中不允許HashMap呼叫任何會修改結構的方法,否則最後會丟擲異常
* @param action 指定動作
*/
public final void forEach(Consumer<? super K> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e.key);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}
/**
* 獲取包含所有值的物件
* @return 包含所有值的物件
*/
public Collection<V> values() {
Collection<V> vs = values;
if (vs == null) {
vs = new Values();
values = vs;
}
return vs;
}
/**
* 包含所有值的物件
* 和上面的集合類似,就不做介紹了
*/
final class Values extends AbstractCollection<V> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<V> iterator() { return new ValueIterator(); }
public final boolean contains(Object o) { return containsValue(o); }
public final Spliterator<V> spliterator() {
return new ValueSpliterator<>(HashMap.this, 0, -1, 0, 0);
}
public final void forEach(Consumer<? super V> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e.value);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}
/**
* 獲取包含所有鍵值對的集合
* @return 包含所有鍵值對的集合
*/
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}
/**
* 包含所有鍵值對的集合
* 和上面的程式碼大同小異,不講解了
*/
final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<Map.Entry<K,V>> iterator() {
return new EntryIterator();
}
public final boolean contains(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>) o;
Object key = e.getKey();
Node<K,V> candidate = getNode(hash(key), key);
return candidate != null && candidate.equals(e);
}
public final boolean remove(Object o) {
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>) o;
Object key = e.getKey();
Object value = e.getValue();
return removeNode(hash(key), key, value, true, true) != null; //注意這裡,倒數第二個引數matchValue為true,說明刪除的時候還要比較value值是否相同
}
return false;
}
public final Spliterator<Map.Entry<K,V>> spliterator() {
return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0);
}
public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}
/**
* 通過指定鍵獲取節點並替換值
* 該方法會比較舊值是否與獲取到的節點的值相同,只有相同的情況下才會替換
* @param key 指定鍵
* @param oldValue 舊值
* @param newValue 新值
* @return 是否替換成功
*/
@Override
public boolean replace(K key, V oldValue, V newValue) {
Node<K,V> e; V v;
if ((e = getNode(hash(key), key)) != null &&
((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
e.value = newValue;
afterNodeAccess(e);
return true;
}
return false;
}
/**
* 通過指定鍵獲取節點並替換值
* 該方法不會比較值是否相等
* @param key 指定鍵
* @param value 新值
* @return 舊值或null
*/
@Override
public V replace(K key, V value) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) != null) {
V oldValue = e.value;
e.value = value;
afterNodeAccess(e);
return oldValue;
}
return null;
}
/**
* 通過指定鍵獲取節點並執行指定動作
* 獲取到的節點若不為空同時值不為空,則不會執行指定動作
* 否則執行指定動作獲取新值,若新值為空直接返回,若新值不為空則繼續
* 若是節點不為空,說明是值為空,則用新值代替
* 若是節點為空,則判斷當前位置是否是紅黑樹結構,若是則採用紅黑樹的方式新增節點,若不是則採用頭插法(將新增的節點插入到連結串列的頭部),同時還要判斷連結串列的長度是否超過8,超過則轉換成紅黑樹
* @param key 指定鍵值、
* @param mappingFunction 指定動作
* @return 舊值或執行指定動作後的新值
*/
@Override
public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {
if (mappingFunction == null)
throw new NullPointerException();
int hash = hash(key);
Node<K,V>[] tab; Node<K,V> first; int n, i;
int binCount = 0;
TreeNode<K,V> t = null;
Node<K,V> old = null;
if (size > threshold || (tab = table) == null ||
(n = tab.length) == 0)
n = (tab = resize()).length; //初始化容量大小或擴容
if ((first = tab[i = (n - 1) & hash]) != null) {
if (first instanceof TreeNode)
old = (t = (TreeNode<K,V>)first).getTreeNode(hash, key);
else {
Node<K,V> e = first; K k;
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
old = e;
break;
}
++binCount;
} while ((e = e.next) != null);
}
V oldValue;
if (old != null && (oldValue = old.value) != null) { //通過鍵獲取到的節點若不為空同時值不為空,則不會執行指定動作
afterNodeAccess(old);
return oldValue;
}
}
V v = mappingFunction.apply(key);
if (v == null) {
return null;
} else if (old != null) { //走到這裡說明節點的值為空,替換成新值
old.value = v;
afterNodeAccess(old);
return v;
}
else if (t != null) //當前位置的結構是紅黑樹
t.putTreeVal(this, tab, hash, key, v);
else { //採用頭插法
tab[i] = newNode(hash, key, v, first);
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
}
++modCount;
++size;
afterNodeInsertion(true);
return v;
}
/**
* 通過指定鍵獲取節點並執行指定動作
* 若是節點為空或節點的值為空,則直接返回
* 若是節點不為空同時值不為空,執行指定動作獲取新值,若新值為空則刪除當前節點,否則替換當前節點的值
* @param key 指定鍵值
* @param mappingFunction 指定動作
* @return null或執行指定動作後的新值
*/
public V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
if (remappingFunction == null)
throw new NullPointerException();
Node<K,V> e; V oldValue;
int hash = hash(key);
if ((e = getNode(hash, key)) != null &&
(oldValue = e.value) != null) {
V v = remappingFunction.apply(key, oldValue);
if (v != null) {
e.value = v;
afterNodeAccess(e);
return v;
}
else
removeNode(hash, key, null, false, true);
}
return null;
}
/**
* 通過指定鍵獲取節點並執行指定動作
* 不管節點是否為空,都會執行指定動作獲取新址
* 若節點不為空且新值不為空,則進行替換
* 若節點不為空且新值為空,則移除當前節點
* 若節點為空且新值不為空,則新增節點,當前位置是紅黑樹結構則採用紅黑樹的新增方式,否則採用頭插法來新增節點
* @param key 指定鍵值
* @param remappingFunction 指定動作
* @return 執行指定動作後的新值
*/
@Override
public V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
if (remappingFunction == null)
throw new NullPointerException();
int hash = hash(key);
Node<K,V>[] tab; Node<K,V> first; int n, i;
int binCount = 0;
TreeNode<K,V> t = null;
Node<K,V> old = null;
if (size > threshold || (tab = table) == null ||
(n = tab.length) == 0)
n = (tab = resize()).length;
if ((first = tab[i = (n - 1) & hash]) != null) {
if (first instanceof TreeNode) //在紅黑樹中查詢
old = (t = (TreeNode<K,V>)first).getTreeNode(hash, key);
else {
Node<K,V> e = first; K k;
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
old = e;
break;
}
++binCount;
} while ((e = e.next) != null);
}
}
V oldValue = (old == null) ? null : old.value;
V v = remappingFunction.apply(key, oldValue);
if (old != null) {
if (v != null) {
old.value = v;
afterNodeAccess(old);
}
else
removeNode(hash, key, null, false, true);
}
else if (v != null) {
if (t != null) //當前位置是紅黑樹結構
t.putTreeVal(this, tab, hash, key, v);
else { //採用頭插法
tab[i] = newNode(hash, key, v, first);
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
}
++modCount;
++size;
afterNodeInsertion(true);
}
return v;
}
/**
* 通過指定鍵獲取節點並執行指定動作
* 若節點不為空且值不為空,則執行指定動作獲取新值
* 若節點不為空且值為空,則採用預設值(value)作為新值
* 若新值為空則移除節點,若移除不為空則替換
* 若節點為空且新值不為空,則新增節點,當前位置是紅黑樹結構則採用紅黑樹的新增方式,否則採用頭插法來新增節點
* @param key 指定鍵值
* @param value 預設值
* @param remappingFunction 指定動作
* @return 執行指定動作後的新值
*/
@Override
public V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
if (value == null)
throw new NullPointerException();
if (remappingFunction == null)
throw new NullPointerException();
int hash = hash(key);
Node<K,V>[] tab; Node<K,V> first; int n, i;
int binCount = 0;
TreeNode<K,V> t = null;
Node<K,V> old = null;
if (size > threshold || (tab = table) == null ||
(n = tab.length) == 0)
n = (tab = resize()).length;
if ((first = tab[i = (n - 1) & hash]) != null) {
if (first instanceof TreeNode)
old = (t = (TreeNode<K,V>)first).getTreeNode(hash, key);
else {
Node<K,V> e = first; K k;
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
old = e;
break;
}
++binCount;
} while ((e = e.next) != null);
}
}
if (old != null) {
V v;
if (old.value != null)
v = remappingFunction.apply(old.value, value);
else
v = value;
if (v != null) {
old.value = v;
afterNodeAccess(old);
}
else
removeNode(hash, key, null, false, true);
return v;
}
if (value != null) {
if (t != null)
t.putTreeVal(this, tab, hash, key, value);
else {
tab[i] = newNode(hash, key, value, first);
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
}
++modCount;
++size;
afterNodeInsertion(true);
}
return value;
}
/**
* 遍歷雜湊表並執行指定動作
* @param action 指定動作
*/
@Override
public void forEach(BiConsumer<? super K, ? super V> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e.key, e.value);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
/**
* 遍歷雜湊表並執行指定動作後獲取新值,利用新值替換所有節點的舊值
* @param function 指定動作
*/
@Override
public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
Node<K,V>[] tab;
if (function == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next) {
e.value = function.apply(e.key, e.value);
}
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
/**
* 淺拷貝
* @return 克隆後的新物件
*/
@SuppressWarnings("unchecked")
@Override
public Object clone() {
HashMap<K,V> result;
try {
result = (HashMap<K,V>)super.clone();
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
result.reinitialize(); //恢復到初始化
//恢復到初始化後需要重新設定節點,明明一開始已經設定節點了為什麼還要恢復初始化後再設定呢?個人認為是因為某些成員屬性需要被恢復到初始化,克隆後的物件有可能會被使用到,不能在與克隆前的物件有所關聯,初始化便需要重新設定
result.putMapEntries(this, false);
return result;
}
/**
* 獲取載入因子
* @return 載入因子
*/
final float loadFactor() { return loadFactor; }
/**
* 獲取容量大小
* @return 容量大小
*/
final int capacity() {
return (table != null) ? table.length :
(threshold > 0) ? threshold :
DEFAULT_INITIAL_CAPACITY;
}
/**
* 自定義序列化
* @param s 輸出流
*/
private void writeObject(java.io.ObjectOutputStream s)
throws IOException {
int buckets = capacity();
// Write out the threshold, loadfactor, and any hidden stuff
s.defaultWriteObject();
s.writeInt(buckets);
s.writeInt(size);
internalWriteEntries(s);
}
/**
* 將鍵值對儲存到流中
* @param s 輸出流
*/
void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
Node<K,V>[] tab;
if (size > 0 && (tab = table) != null) {
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next) {
s.writeObject(e.key);
s.writeObject(e.value);
}
}
}
}
/**
* 自定義反序列化
* @param s 輸入流
*/
private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException {
// Read in the threshold (ignored), loadfactor, and any hidden stuff
s.defaultReadObject();
reinitialize();
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new InvalidObjectException("Illegal load factor: " +
loadFactor);
s.readInt(); // 讀取buckets
int mappings = s.readInt(); // 讀取size
if (mappings < 0)
throw new InvalidObjectException("Illegal mappings count: " +
mappings);
else if (mappings > 0) { // (if zero, use defaults)
// Size the table using given load factor only if within
// range of 0.25...4.0
float lf = Math.min(Math.max(0.25f, loadFactor), 4.0f);
float fc = (float)mappings / lf + 1.0f; //向上取整獲取初始容量,儘可能的獲取到更大的容量以便減少resize的呼叫
int cap = ((fc < DEFAULT_INITIAL_CAPACITY) ?
DEFAULT_INITIAL_CAPACITY :
(fc >= MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY :
tableSizeFor((int)fc));
float ft = (float)cap * lf;
threshold = ((cap < MAXIMUM_CAPACITY && ft < MAXIMUM_CAPACITY) ?
(int)ft : Integer.MAX_VALUE);
// Check Map.Entry[].class since it's the nearest public type to
// what we're actually creating.
SharedSecrets.getJavaOISAccess().checkArray(s, Map.Entry[].class, cap);
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] tab = (Node<K,V>[])new Node[cap];
table = tab;
// Read the keys and values, and put the mappings in the HashMap
for (int i = 0; i < mappings; i++) {
@SuppressWarnings("unchecked")
K key = (K) s.readObject();
@SuppressWarnings("unchecked")
V value = (V) s.readObject();
putVal(hash(key), key, value, false, false);
}
}
}
/**
* 迭代器基類
*/
abstract class HashIterator {
//下一個節點
Node<K,V> next;
//當前節點
Node<K,V> current;
//記錄修改次數
int expectedModCount;
//當前位置
int index;
/**
* 初始化
* 提前準備好第一個節點
*/
HashIterator() {
expectedModCount = modCount;
Node<K,V>[] t = table;
current = next = null;
index = 0;
if (t != null && size > 0) { // advance to first entry
do {} while (index < t.length && (next = t[index++]) == null);
}
}
/**
* 是否存在下一個節點
* @return 是否存在下一個節點
*/
public final boolean hasNext() {
return next != null;
}
/**
* 獲取下一個節點
* @return 下一個節點
*/
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
if ((next = (current = e).next) == null && (t = table) != null) {
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
/**
* 移除當前節點
*/
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount;
}
}
/**
* 包含所有鍵的迭代器
*/
final class KeyIterator extends HashIterator
implements Iterator<K> {
public final K next() { return nextNode().key; }
}
/**
* 包含所有值的迭代器
*/
final class ValueIterator extends HashIterator
implements Iterator<V> {
public final V next() { return nextNode().value; }
}
/**
* 包含所有鍵值對的迭代器
*/
final class EntryIterator extends HashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode(); }
}
/**
* 建立普通節點
*/
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}
/**
* 將紅黑樹節點轉換成普通節點
*/
Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
return new Node<>(p.hash, p.key, p.value, next);
}
/**
* 建立紅黑樹節點
*/
TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
return new TreeNode<>(hash, key, value, next);
}
/**
* 將普通節點節點轉換成紅黑樹節點
*/
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}
/**
* 恢復到初始狀態
*/
void reinitialize() {
table = null;
entrySet = null;
keySet = null;
values = null;
modCount = 0;
threshold = 0;
size = 0;
}
/**
* 訪問節點後的動作
*/
void afterNodeAccess(Node<K,V> p) { }
/**
* 插入節點後的動作
*/
void afterNodeInsertion(boolean evict) { }
/**
* 刪除節點後的動作
*/
void afterNodeRemoval(Node<K,V> p) { }
/**
* 獲取根節點
* @return 根節點
*/
final TreeNode<K,V> root() {
for (TreeNode<K,V> r = this, p;;) {
if ((p = r.parent) == null)
return r;
r = p;
}
}
/**
* 校驗紅黑樹的基本特性
* @return 是否是紅黑樹
*/
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;
}
獲取節點
/**
* 通過鍵獲取對應的值
* @param key 指定鍵
* @return 鍵對應的值
*/
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* 通過鍵獲取對應的值
* 因為雜湊表允許存放null,若獲取的值為null則採用預設值
* @param key 指定鍵
* @param defaultValue 預設值
* @return 鍵對應的值或預設值
*/
@Override
public V getOrDefault(Object key, V defaultValue) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;
}
新增節點
/**
* 新增指定鍵值對
* 與putIfAbsent相比該方法的修改一定會生效,不管值是否為空
* @param key 指定鍵
* @param value 指定值
* @return 舊值或null
*/
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* 新增指定節點到雜湊表中
* 有可能會發生重複新增節點,若發生重複的話會更新指定節點的最新值,若不重複則新增即可,以下的新增方式大同小異,區別在於紅黑樹可能要做一些調整來維持紅黑樹的特性
* 若索引上不存在任何節點直接新增即可
* 若索引上已經存在節點且結構是連結串列,若連結串列長度不超過8則往連結串列上新增即可,若超過8則執行樹形化操作
* 若索引上已經存在節點且結構是紅黑樹,呼叫紅黑樹的新增節點方法
* 返回值是null表示沒有重複新增,返回具體值表示發生重複新增
* @param hash 雜湊值
* @param key 指定鍵
* @param value 指定值
* @param onlyIfAbsent 在鍵值對存在的情況下發生重複時新增是否不允許修改值,為true則表示不允許
* @param evict 模式
* @return 舊值或null
*/
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length; //初始化容量大小
if ((p = tab[i = (n - 1) & hash]) == null) //指定索引上未存放元素,直接存放即可
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p; //通過hash與equals來判斷是否是重複的節點,若是則先記錄下當前節點以便後續替換值
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); //以紅黑樹的方式進行新增節點
else { //不重複的節點但在同一個索引處下有兩種可能:hash不同、hash相同但equals不相同
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // 當連結串列長度超過8
treeifyBin(tab, hash); //樹形化操作
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // 新增節點時存在重複的節點,將該節點更新成最新的值
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold) //超過閾值時就需要進行擴容
resize();
afterNodeInsertion(evict);
return null;
}
/**
* 紅黑樹中插入節點
* 插入過程中會判斷是否重複插入,從根節點開始查詢
* 若是返回null表示新增了一個節點,若是返回重複的節點則後續會將這個節點的值修改成最新的
* @param map map物件
* @param tab 雜湊表
* @param h 指定節點的雜湊值
* @param k 指定節點的鍵
* @param v 指定節點的值
* @return null或重複的節點
*/
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) //當前節點的hash值大於指定節點的hash,後續應該從左子樹開始查詢
dir = -1;
else if (ph < h) //當前節點的hash值小於指定節點的hash,後續應該從右子樹開始查詢
dir = 1;
else if ((pk = p.key) == k || (k != null && k.equals(pk))) //當前節點的hash值與指定節點的hash值相同、equals也相等,此時的節點就是重複的節點,直接返回即可
return p;
//走到這裡說明當前節點的hash值與指定節點的hash值相同,但是equals不相等,而後續不知道該從左子樹還是右子樹開始查詢,故而要指定具體規則,通過comparable來比較當前節點的鍵與指定節點的鍵
//但前提是要先檢測是否實現了comparable才能比較
//繼而還要檢測兩個鍵值的型別是否相同,否則也不具備可比性
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0) {
/**
* 走到這裡的話說明不具備比較器或者比較之後還是相等或者兩個鍵值的型別不同,那麼還是無法決定是要從左子樹還是右子樹開始查詢,所以沒辦法只能先嚐試查抄左子樹在查詢右子樹,個個去嘗試下
* searched:標識已經遍歷過當前節點的所有子節點,包括子孫節點,為false說明還沒有過,那麼就遞迴遍歷對比,看是否能夠查詢到equals相等的節點,如果查詢到了,也就是查詢到了重複節點,直接返回即可
* 如果查詢不到,說明應該新增一個節點
* 在查詢過後就修改此值,標識已經遍歷過了後續就不用再遍歷了
*/
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;
}
//走到這裡說明遍歷了整個紅黑樹都沒有找到鍵值相等的節點,說明該新增一個節點了,那就要思考要新增到左子樹上還是右子樹上呢?
//而該方法就是決定勝負的那個方法,必須要抉擇出是左子樹還是右子樹
//先比較鍵的類名是否相等,若相等則比較雜湊值並返回結果,若不相等則返回結果,這樣子就能決定是新增到左子樹還是右子樹上
dir = tieBreakOrder(k, pk);
}
//走到這裡說明要新增一個節點,而新增一個節點應該先找到葉子節點才能實現最後的插入,找到葉子節點並插入後就是調整節點之間的關係,比較容易理解
//若dir小於或等於0且當前節點的左子樹是否為空,若不為空則繼續查詢,若為空則說明新增的節點可以作為當前節點的左子樹
//若dir大於0且當前節點的右子樹是否為空,若不為空則繼續查詢,若為空則說明新增的節點可以作為當前節點的右子樹
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;
}
}
}
/**
* 批量新增節點
* @param m 指定集合
*/
public void putAll(Map<? extends K, ? extends V> m) {
putMapEntries(m, true);
}
/**
* 新增指定鍵值對
* 與put相比該方法的修改只有在值為空的情況下才會生效
* @param key 指定鍵
* @param value 指定值
* @return 舊值或null
*/
@Override
public V putIfAbsent(K key, V value) {
return putVal(hash(key), key, value, true, true);
}
移除節點
/**
* 通過指定鍵移除節點
* matchVaue = false,故移除過程中不會比較值是否相等
* @param key 指定鍵
* @return 移除節點的值
*/
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
/**
* 通過指定鍵移除節點
* matchVaue = true,故移除過程中還會比較值是否相等
* @param key 指定鍵
* @return 是否移除成功
*/
@Override
public boolean remove(Object key, Object value) {
return removeNode(hash(key), key, value, true, true) != null;
}
e.hash & oldCap:其結果不為0個人稱之為高位,等於0個人稱之為低位。實際上它是在判斷新容量下節點的索引位置應該是保持不變還是變成原先的索引(舊容量下的索引位置) + 舊容量大小,至於為什麼是這兩種可以看上面具體方法的解釋。
總結
-
當前位置不存在節點時新增節點
:直接新增。 -
當前位置存在節點且是紅黑樹結構下新增節點
:直接新增,新增後可能需要旋轉並移動根節點。 -
當前位置存在節點且是連結串列結構下新增節點
:直接新增,新增後檢測是否超過紅黑樹的閾值(8)和當前雜湊表的長度是否超過一定容量大小(64),若這兩者都滿足則先將其單向連結串列轉換成雙向連結串列,遍歷雙向連結串列後變成紅黑樹,此時紅黑樹中的節點具有prev、next、lef、right、parent資訊。 -
當前位置只有一個節點且不存在連結串列或紅黑樹結構下進行擴容
:在新的雜湊表中重新計算位置並填充即可。 -
當前位置有多個節點且是紅黑樹結構下進行擴容
:將紅黑樹嘗試分成高低位兩棵樹(何為高低位可參考上面的解釋),低位這棵樹先檢測長度是否超過連結串列的閾值(6),若超過說明要進行樹形化,接著檢測是否已經樹形化過,實際上就是判斷高位的那棵樹是否為空即可知道低位是否已經樹形化過,若是高位那棵樹為空,說明紅黑樹並未分成高低位兩棵樹,所以最終是將紅黑樹填充到新的雜湊表中;同樣的,若是高位那棵樹存在,說明紅黑樹確實分成了兩棵樹,結構已經發生了變化,低位需要重新樹形化;若低位這棵樹的長度不超過連結串列的閾值(6)則要將其轉換成單向連結串列結構並填充到新的雜湊表中。高位那棵樹的做法和低位是類似的,只不過最終填充到新的雜湊表中時索引不一致,低位的索引是原先的索引(保持不變),高位的索引是原來的索引 + 舊的雜湊表的容量大小。 -
當前位置有多個節點且是連結串列結構下進行擴容
:將連結串列嘗試分成高低位兩條連結串列並填充到新的雜湊表中,低位的索引是原先的索引(保持不變),高位的索引是原來的索引 + 舊的雜湊表的容量大小。 -
當前位置存在節點且是紅黑樹結構下刪除節點
:查詢到指定節點後先調整雙向連結串列,若此時紅黑樹的長度過小則直接將其轉換成單向連結串列,若不是則利用交換樹連結的方式來移除並調整平衡。該內容涉及到比較多的演算法,建議對於演算法只要懂的它做什麼即可,後續想研究演算法的話可以在回過頭來! -
HashMap的鍵與值都允許存放Null。
-
HashMap是無序不可重複、非執行緒安全。
-
HashMap的容量大小必須是2的冪次方。
-
HashMap預設初始容量16、預設載入因子0.75。
-
HashMap中索引的計算方式:hash & (capacity - 1)。
-
HashMap擴容時會以
2倍大小
進行增長,舊陣列中的節點重新雜湊到新陣列中,而在新陣列中索引可能出現在原索引位置或原索引位置 + 舊陣列容量大小。 -
紅黑樹的根節點不一定是連結串列的頭節點。
-
多執行緒下,JDK1.7在擴容時會出現連結串列節點的互相引用導致了死迴圈,同時也會造成資料丟失,而JDK1.8會造成資料丟失。
重點關注
單向連結串列轉成紅黑樹
紅黑樹轉成單向連結串列
rehash
預設初始容量與載入因子
hash值與索引的計算方式