Map源碼剖析
HashMap&LinkedHashMap&Hashtable
hashMap默認的閾值是0.75
HashMap put操作
put操作涉及3種結構,普通node節點,鏈表節點,紅黑樹節點,針對第三種,紅黑樹節點,我們後續單獨去學習,這裏不多做擴散
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) {
// 初始化哈希數組,或者對哈希數組擴容,返回新的哈希數組
tab = resize();
n = tab.length;
}
// 相當於取餘
i = (n - 1) & hash;
p = tab[i];
if (p == 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)))) {
// 存在同位元素,也就是出現了hash碰撞
e = p;
} else if (p instanceof TreeNode) {
// 如果當前位置已經是紅黑樹節點,那麼就put紅黑色
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
} else {
// 遍歷哈希槽後面鏈接的其他元素(binCount統計的是插入新元素之前遍歷過的元素數量)
// 這裏就是鏈表類型
for (int binCount = 0; ; ++binCount) {
// 後繼節點爲空
if ((e = p.next) == null) {
// 拼接到後繼節點上
p.next = newNode(hash, key, value, null);
/**
* 哈希槽(鏈)上的元素數量增加到TREEIFY_THRESHOLD後,這些元素進入波動期,即將從鏈表轉換爲紅黑樹
* 注意這個TREEIFY_THRESHOLD 是8,爲什麼是8??
* 每次遍歷一個鏈表,平均查找的時間複雜度是 O(n),n 是鏈表的長度。由於紅黑樹有自平衡的特點,可以防止不平衡情況的發生,
* 所以可以始終將查找的時間複雜度控制在 O(log(n))。
* 最初鏈表還不是很長,所以可能 O(n) 和 O(log(n)) 的區別不大,但是如果鏈表越來越長,那麼這種區別便會有所體現。所以爲了提升查找性能,需要把鏈表轉化爲紅黑樹的形式。
* 鏈表查詢的時候使用二分查詢,平均查找長度爲n/2,長度爲8的時候,爲4,而6/2 = 3
* 而如果是紅黑樹,那麼就是log(n) ,長度爲8時候,log(8) = 3, log(6) =
* 這個時候我們發現超過8這個閾值之後,鏈表的查詢效率會越來越不如紅黑樹
*/
if (binCount >= TREEIFY_THRESHOLD - 1) {
// -1 for 1st
treeifyBin(tab, hash);
}
break;
}
// 判斷鏈表中的後繼原始是否hash碰撞,如果發生了hash碰撞break
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 如果存在同位元素(在HashMap中佔據相同位置的元素)
if (e != null) { // existing mapping for key
V oldValue = e.value;
// 判斷是否需要進行覆蓋取值,因爲key相同,那麼直接取代,否則什麼也不操作
if (!onlyIfAbsent || oldValue == null) {
e.value = value;
}
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
總結關鍵信息:
哈希槽(鏈)上的元素數量增加到TREEIFY_THRESHOLD後,這些元素進入波動期,即將從鏈表轉換爲紅黑樹
注意這個TREEIFY_THRESHOLD 是8,爲什麼是8??
每次遍歷一個鏈表,平均查找的時間複雜度是 O(n),n 是鏈表的長度。由於紅黑樹有自平衡的特點,可以防止不平衡情況的發生,
所以可以始終將查找的時間複雜度控制在 O(log(n))。
最初鏈表還不是很長,所以可能 O(n) 和 O(log(n)) 的區別不大,但是如果鏈表越來越長,那麼這種區別便會有所體現。所以爲了提升查找性能,需要把鏈表轉化爲紅黑樹的形式。
鏈表查詢的時候使用二分查詢,平均查找長度爲n/2,長度爲8的時候,爲4,而6/2 = 3
而如果是紅黑樹,那麼就是log(n) ,長度爲8時候,log(8) = 3, log(6) =
這個時候我們發現超過8這個閾值之後,鏈表的查詢效率會越來越不如紅黑樹
HashMap get,remove操作
除了紅黑樹的查找比較特殊,其餘的鏈表查找就是暴力搜索,只是平均下來找到一個元素的話是n/2
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab = table;
Node<K,V> p;
int n, index;
if (tab != 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;
} while ((e = e.next) != null);
}
}
// 移除節點,可能只需要匹配hash和key就行,也可能還要匹配value,這取決於matchValue參數
if (node != null && (!matchValue || (v = node.value) == 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.next = node.next;
}
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
HashMap擴容
鏈表拆分,進入新的容器
這裏有個知識點:如何使用位運算進行取模
a % b == a & (b - 1)
我們拆分鏈表的思路也是這樣:比如原來長度爲8的鏈表,也就是 x % 8 = x & (8 - 1) = x & 0111 也就是取後三位,那麼擴容之後重新排序的話,容量擴大一倍,也就是16,那麼這個時候就是 x % 16 = x & (16 - 1) = x & 1111 這個時候我們發現和之前的區別就是最高位由原來的0變爲1,如果還在後三位範圍內,那麼新容量中的位置是不會變的
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) {
// 如果已經達到1 << 30;,那麼直接設置爲Integer.MAX_VALUE; 0x7fffffff
threshold = Integer.MAX_VALUE;
return oldTab;
} else {
// mod by xiaof 嘗試將哈希表數組容量加倍,注意這裏是左移,也就是說*2
newCap = oldCap << 1;
// 如果容量成功加倍(沒有達到上限),則將閾值也加倍
if (newCap < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) {
newThr = oldThr << 1;
}
}
// else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
// oldCap >= DEFAULT_INITIAL_CAPACITY) {
// newThr = oldThr << 1; // double threshold
// }
} else if (oldThr > 0) {
// initial capacity was placed in threshold
newCap = oldThr;
} else { // zero initial threshold signifies using defaults
// 如果實例化HashMap時沒有指定初始容量,則使用默認的容量與閾值
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
/*
* 至此,如果newThr==0,則可能有以下兩種情形:
* 1.哈希數組已經初始化,且哈希數組的容量還未超出最大容量,
* 但是,在執行了加倍操作後,哈希數組的容量達到了上限
* 2.哈希數組還未初始化,但在實例化HashMap時指定了初始容量
*/
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
// 如果新容量小於最大允許容量,並且新容量*裝載因子之後還是小於最大容量,那麼說明不需要擴容,那麼直接使用ft作爲新的閾值容量
// 如果新容量已經超過最大容量了,那麼就直接返回最大允許的容量
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
// 更新閾值
threshold = newThr;
// 新的容器對象,創建容量爲新的newCap
@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 = oldTab[j];
if (e != null) {
// 把原來的數組中的指針設置爲空
oldTab[j] = null;
if (e.next == null) {
// 重新計算hash索引位置,計算hash位置的方式防止數組越界的話,那麼就設置hashcode & 長度 - 1
newTab[e.hash & (newCap - 1)] = e;
} else if (e instanceof TreeNode) {
// 紅黑樹,這裏是對紅黑樹進行拆分
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
} else { // preserve order
// lo對應的鏈表是數據不會動的
Node<K,V> loHead = null, loTail = null;
// hi對應的鏈表標識是需要去新容器新的位置的
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
// 這個是鏈表的情況下進行拆分
// 因爲num % 2^n == num & (2^n - 1),容量大小一定是2的N次方
do {
next = e.next;
// 注意:e.hash & oldCap,注意這裏是對老的容量oldCap進行計算這一步就是前面說的判斷多出的這一位是否爲1
// 因爲新的是老的2倍,新節點位置是否需要發生改變,取決於最高位是否爲0
// 若與原容量做與運算,結果爲0,表示將這個節點放入到新數組中,下標不變
// 由於原來的是2的倍數,那麼取餘肯定是和一個0111111的對象進行&操作,而不減一那就是10000000進行&操作,正好是最高位
if ((e.hash & oldCap) == 0) {
// 最高位爲0,那麼位置不需要改變,本身就在原來容量範圍內的數據
// 直接加入lotail,並判斷是否需要初始化lotail
if (loTail == null) {
loHead = e;
} else {
loTail.next = e;
}
loTail = e;
} else {
// 最高位是1,那麼就需要進行切換位置
if (hiTail == null) {
hiHead = e;
} else {
hiTail.next = e;
}
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
// 最後返回最新的容器對象
return newTab;
}
LinkedHashMap
基本和hashmap差不多,唯一需要注意下的是
還有一個核心點就是linkedHashMap覆蓋了newNode方法
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
// 這裏創建了linkedhashmap對象
LinkedHashMap.Entry<K,V> p = new LinkedHashMap.Entry<K,V>(hash, key, value, e);
// 創建完成之後,就添加到鏈表中連接起來
linkNodeLast(p);
return p;
}
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;
tail = p;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
}
插入覆蓋afterNodeAccess
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) {
// 獲取節點 b -> p -> a
LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
// 斷開尾部連接
p.after = null;
// 如果前置節點是空的,那麼就吧A作爲head節點
if (b == null) {
head = a;
} else {
// 如果前置節點不爲空,那麼就吧前置節點連接到後置節點,吧中間節點斷開
b.after = a;
}
// 後置節點不爲空,那麼就吧後置節點連接到前置節點上
if (a != null) {
a.before = b;
} else {
// 如果後置節點爲空,那麼重新設置tail指向before節點
last = b;
}
// 重新連接當前這個節點到末尾
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
afterNodeInsertion在linkedhashmap中作用不大
/**
*
* +----+ +----+ +----+
* | b | ---> | p | ---> | a |
* +----+ +----+ +----+
* @param e
*/
void afterNodeRemoval(Node<K,V> e) { // unlink
// 移除節點
LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.before = p.after = null;
if (b == null) {
head = a;
} else {
b.after = a;
}
if (a == null) {
tail = b;
} else {
a.before = b;
}
}
綜上:linkedhashmap相對hashmap其實就是多加了一個鏈表把所有的數據關聯起來,只有在遍歷的時候才能體現出來有序,其他的操作是沒有差別的
關於hashtable
首先hashtable是線程安全的,因爲它所有的函數都加上了synchronized
鏈表頭插法,沒有紅黑樹的轉換
初始化容量的時候默認是11,是奇數,而hashmap全都是2的冪次方
hashtable允許key爲null
rehash函數
常用的hash函數是選一個數m取模(餘數),這個數在課本中推薦m是素數,但是經常見到選擇m=2n,因爲對2n求餘數更快,並認爲在key分佈均勻的情況下,key%m也是在[0,m-1]區間均勻分佈的。但實際上,key%m的分佈同m是有關的。
證明如下: key%m = key - xm,即key減掉m的某個倍數x,剩下比m小的部分就是key除以m的餘數。顯然,x等於key/m的整數部分,以floor(key/m)表示。假設key和m有公約數g,即key=ag, m=bg, 則 key - xm = key - floor(key/m)m = key - floor(a/b)m。由於0 <= a/b <= a,所以floor(a/b)只有a+1中取值可能,從而推導出key%m也只有a+1中取值可能。a+1個球放在m個盒子裏面,顯然不可能做到均勻。
由此可知,一組均勻分佈的key,其中同m公約數爲1的那部分,餘數後在[0,m-1]上還是均勻分佈的,但同m公約數不爲1的那部分,餘數在[0, m-1]上就不是均勻分佈的了。把m選爲素數,正是爲了讓所有key同m的公約數都爲1,從而保證餘數的均勻分佈,降低衝突率。
protected void rehash() {
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
// overflow-conscious code
// 這裏重新計算容量的辦法是容量擴大一倍,然後+1
int newCapacity = (oldCapacity << 1) + 1;
if (newCapacity - MAX_ARRAY_SIZE > 0) {
if (oldCapacity == MAX_ARRAY_SIZE) {
// Keep running with MAX_ARRAY_SIZE buckets
return;
}
newCapacity = MAX_ARRAY_SIZE;
}
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
modCount++;
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;
// 重新把舊的原始轉移到新數組上
for (int i = oldCapacity ; i-- > 0 ;) {
for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
Entry<K,V> e = old;
old = old.next;
// 這裏因爲容量是奇數,那麼就需要使用%取餘,而不是位運算 -》 a & (b - 1)
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
e.next = (Entry<K,V>)newMap[index];
newMap[index] = e;
}
}
}
參考
https://www.cnblogs.com/tuyang1129/p/12368842.html -- 鏈表拆分
https://www.cnblogs.com/lyhc/p/10743550.html - linkedhashmap
ConcurrentHashMap
put操作
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
// 本質上和hashmap沒有什麼差別,都是把hashcode進行對半異或,這樣就可以用一半的位數,集合了32位長度的信息
int hash = spread(key.hashCode());
int binCount = 0;
Node<K,V>[] tab = table;
// 這裏循環的目的是cas的重試操作
for (;;) {
// 指向待插入元素應當插入的位置
Node<K,V> f;
// 元素f對應的哈希值
int fh;
// 當前hash表數組的長度容量
int n;
int i;
// 如果哈希數組還未初始化,或者容量無效,則需要初始化一個哈希數組
if (tab == null || (n = tab.length) == 0) {
tab = initTable();
// 這裏n -1 & hash 是經典取餘操作,參考之前hashmap
} else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 如果當前位置是空的,那麼就可以通過cas設置對應的值
Node<K, V> newNode = new Node<K,V>(hash, key, value, null);
// 用一次 CAS 操作將這個新值放入其中即可,這個 put 操作差不多就結束了,可以拉到最後面了
// 如果 CAS 失敗,那就是有併發操作,進到下一個循環就好了
if (casTabAt(tab, i, null, newNode)) {
// 插入完成,跳出循環,如果更新失敗,重新循環進入
break; // no lock when adding to empty bin
}
} else if ((fh = f.hash) == MOVED) {
/*
* 如果待插入元素所在的哈希槽上已經有別的結點存在,且該結點類型爲MOVED
* 說明當前哈希數組正在擴容中,此時,可以嘗試加速擴容過程
*/
tab = helpTransfer(tab, f);
} else {
V oldVal = null;
// 這裏避免併發,對f節點的引用進行上鎖
synchronized (f) {
// 如果tab[i]==f,則代表當前待插入狀態仍然可信
if (tabAt(tab, i) == f) {
// fh > 0 標識不是在擴容,是正常節點
if (fh >= 0) {
binCount = 1;
// 遍歷鏈表
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 找到對應的值
if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
// 記錄同位元素舊值
oldVal = e.val;
// 如果允許覆蓋,則存入新值
if (!onlyIfAbsent) {
e.val = value;
}
// 跳出循環
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
} else if (f instanceof TreeBin) {
// 如果當前哈希槽首個元素是紅黑樹(頭結點)
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
// 如果對已有元素搜索過,則計數會發生變動,這裏需要進一步觀察
if (binCount != 0) {
// 哈希槽(鏈)上的元素數量增加到TREEIFY_THRESHOLD後,這些元素進入波動期,即將從鏈表轉換爲紅黑樹
if (binCount >= TREEIFY_THRESHOLD) {
// 注意,這裏也只是鎖了這一個節點,注意,這裏不一定一定是轉換爲紅黑樹
// 如果整個tab長度是小於64的話,這裏會選擇自動擴容,如果已經超過64了,才考慮轉換紅黑樹
treeifyBin(tab, i);
}
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
get和remove操作
略,就是根據索引找節點
擴容
核心方法就是transfer
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length;
// stride 在單核下直接等於 n,多核模式下爲 (n>>>3)/NCPU,最小值是 16
// 將這 n 個任務分爲多個任務包,每個任務包有 stride 個任務
int stride = (NCPU > 1) ? (n >>> 3) / NCPU : n;
if (stride < MIN_TRANSFER_STRIDE) {
stride = MIN_TRANSFER_STRIDE; // subdivide range
}
if (nextTab == null) { // initiating
try {
// 直接擴大一倍
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
transferIndex = n;
}
int nextn = nextTab.length;
// ForwardingNode 翻譯過來就是正在被遷移的 Node
// 這個構造方法會生成一個Node,key、value 和 next 都爲 null,關鍵是 hash 爲 MOVED
// 後面我們會看到,原數組中位置 i 處的節點完成遷移工作後,
// 就會將位置 i 處設置爲這個 ForwardingNode,用來告訴其他線程該位置已經處理過了
// 所以它其實相當於是一個標誌。, 這個在put的時候會判斷節點hash值,用來判斷是否需要協助擴容
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
// advance 指的是做完了一個位置的遷移工作,可以準備做下一個位置的了
boolean advance = true;
boolean finishing = false; // to ensure sweep before committing nextTab
// 循環所有的節點位置,判斷是否需要進行遷移
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
while (advance) {
int nextIndex, nextBound;
if (--i >= bound || finishing) {
advance = false;
} else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
} else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) {
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
i = n; // recheck before commit
}
}
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
// 真正的開始數據遷移,先對f節點上鎖,f是tab中的一個位置
synchronized (f) {
// 保證數據正確性沒有發生變化
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
// 頭節點的 hash 大於 0,說明是鏈表的 Node 節點
if (fh >= 0) {
// 下面這一塊和 Java7 中的 ConcurrentHashMap 遷移是差不多的,
// 需要將鏈表一分爲二,
// 找到原鏈表中的 lastRun,然後 lastRun 及其之後的節點是一起進行遷移的
// lastRun 之前的節點需要進行克隆,然後分到兩個鏈表中
int runBit = fh & n;
Node<K,V> lastRun = f;
// 循環鏈表,獲取到尾節點
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
if (runBit == 0) {
ln = lastRun;
hn = null;
} else {
hn = lastRun;
ln = null;
}
// 從頭循環到尾部
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash;
K pk = p.key;
V pv = p.val;
// ph是這個節點的hash值&n如果爲0,說明再n-1部分,位置沒變,還是放入之前的位置
if ((ph & n) == 0) {
ln = new Node<K,V>(ph, pk, pv, ln);
} else {
// 不爲0,說明再n-1上面的位置,位置變了,那麼就重新設置位置
hn = new Node<K,V>(ph, pk, pv, hn);
}
}
// 其中的一個鏈表放在新數組的位置 i
setTabAt(nextTab, i, ln);
// 把head放到數組i+n的位置
setTabAt(nextTab, i + n, hn);
// 將原數組該位置處設置爲 fwd,代表該位置已經處理完畢,
// 其他線程一旦看到該位置的 hash 值爲 MOVED,就不會進行遷移了
setTabAt(tab, i, fwd);
advance = true;
} else if (f instanceof TreeBin) {
// 紅黑樹的遷移
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> lo = null, loTail = null;
TreeNode<K,V> hi = null, hiTail = null;
int lc = 0, hc = 0;
for (Node<K,V> e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode<K,V> p = new TreeNode<K,V>
(h, e.key, e.val, null, null);
if ((h & n) == 0) {
if ((p.prev = loTail) == null)
lo = p;
else
loTail.next = p;
loTail = p;
++lc;
}
else {
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin<K,V>(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin<K,V>(hi) : t;
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
}
}
}
}
}
TreeMap
事先說明,這個集合對象實現是基於紅黑樹實現的
紅黑樹是一種近似平衡的二叉查找樹,它能夠確保任何一個節點的左右子樹的高度差不會超過二者中較低那個的一倍
複習一下紅黑樹的定義:
性質1. 結點是紅色或黑色。
性質2. 根結點是黑色。
性質3. 所有葉子都是黑色。(葉子是NIL結點)
性質4. 每個紅色結點的兩個子結點都是黑色。(從每個葉子到根的所有路徑上不能有兩個連續的紅色結點)
性質5. 從任一結點到其每個葉子的所有路徑都包含相同數目的黑色結點。
這些約束強制了紅黑樹的關鍵性質: 從根到葉子的最長的可能路徑不多於最短的可能路徑的兩倍長
put操作
public V put(K key, V value) {
Entry<K, V> t = root;
if (t == null) {
// 如果根節點爲空,然後這個比較其實是起一個類型檢查作用,判斷key能否進行Comparable操作
compare(key, key); // type (and possibly null) check
// 創建root節點
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
// 如果根節點存在
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
// 判斷是否設置了比較器
Comparator<? super K> cpr = comparator;
if (cpr != null) {
// 從根節點循環比對,直到相等
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0) {
// 比目標小,那麼就遍歷進入左節點
t = t.left;
} else if (cmp > 0) {
t = t.right;
} else {
// 如果存在相同key,直接設置值,並返回舊值,並結束後續操作,這裏就不需要進行修復操作了
return t.setValue(value);
}
} while (t != null);
} else {
if (key == null) {
throw new NullPointerException();
}
@SuppressWarnings("unchecked") Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0) {
t = t.left;
} else if (cmp > 0) {
t = t.right;
} else {
return t.setValue(value);
}
} while (t != null);
}
// 如果最終找到葉子節點了,我們就得新增一個節點設置
Entry<K, V> e = new Entry<>(key, value, parent);
if (cmp < 0) {
parent.left = e;
} else {
parent.right = e;
}
// 紅黑樹再平衡***
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
紅黑樹再平衡
我們做如下規定
當前正在處理的節點爲X,父節點P,爺爺節點G,叔叔節點Y,A3標識黑高位3的紅黑樹
1.無需調整
2.僅僅需要考慮父節點爲紅色的情況
case1:Y爲紅色,X可左可右=》P,Y染黑,G染紅,X回溯到G
case2:Y爲黑色,X爲右孩子=》右旋P,X指向P,轉爲case3
case3:Y爲黑色,X爲左孩子=》P染黑,G染紅,右旋G
場景1:
1.父節點爲紅,父節點爲左孩子,叔叔節點爲紅
場景2:
- 父節點爲紅,並且父節點爲左孩,叔叔節點爲黑,當前節點爲右孩子
場景3:
3.父節點爲紅,並且父節點爲左孩,叔叔節點爲黑,當前節點爲左孩子
場景4
4.父節點爲紅,父節點爲右孩子,叔叔節點爲紅
場景5:
5.父節點爲紅,父節點爲右孩子,叔叔節點爲黑,當前節點爲左孩子
場景6:
6.父節點爲紅,父節點爲右孩子,叔叔節點爲黑,當前節點爲右孩子
總結插入後:
1.父節點爲紅,父節點爲左孩子,叔叔節點爲紅 --------------------------------------------------------- (只需要調整顏色)
2.父節點爲紅,父節點爲左孩,叔叔節點爲黑,當前節點爲右孩子-------------------------- (需要左旋,然後右旋)
3.父節點爲紅,父節點爲左孩,叔叔節點爲黑,當前節點爲左孩子-------------------------- (需要直接右旋)
4.父節點爲紅,父節點爲右孩子,叔叔節點爲紅 --------------------------------------------------------- (只需要調整顏色)
5.父節點爲紅,父節點爲右孩,叔叔節點爲黑,當前節點爲左孩子-------------------------- (需要右旋,然後左旋)
6.父節點爲紅,父節點爲右孩,叔叔節點爲黑,當前節點爲右孩子-------------------------- (需要直接左旋)
package com.cutter.collection.study.m12;
import java.util.TreeMap;
/**
* 功能描述
*
* @since 2022-11-25
*/
public class Code009RedBlackTreeTest1 {
private static void test1() {
TreeMap treeMap = new TreeMap();
// 1. 父節點爲左孩子
// 2. 叔叔節點爲紅
treeMap.put("5", "root-BLACK");
treeMap.put("2", "PARENT-RED");
treeMap.put("6", "UNCLE-RED");
treeMap.put("3", "SUB-RED");
}
private static void test2() {
TreeMap treeMap = new TreeMap();
// 1. 父節點爲紅,並且父節點爲左孩子
// 2. 叔叔節點爲黑
treeMap.put(357708, 357708);
treeMap.put(878400, 878400);
treeMap.put(845548, 845548);
treeMap.put(833347, 833347);
treeMap.put(90065, 90065);
treeMap.put(735614, 735614);
treeMap.put(51539, 51539);
treeMap.put(834866, 834866);
treeMap.put(669982, 669982);
}
private static void test3() {
TreeMap treeMap = new TreeMap();
// 1. 父節點爲紅,並且父節點爲左孩子
// 2. 叔叔節點爲黑
// 3. X當前節點是左孩子
treeMap.put(889057, 889057);
treeMap.put(591367, 591367);
treeMap.put(760438, 760438);
treeMap.put(877815, 877815);
treeMap.put(316291, 316291);
treeMap.put(387097, 387097);
treeMap.put(101558, 101558);
treeMap.put(668311, 668311);
treeMap.put(263179, 263179);
treeMap.put(191393, 191393);
}
private static void test4() {
TreeMap treeMap = new TreeMap();
// 1. 父節點爲紅,父節點爲右孩子,叔叔節點爲紅
treeMap.put(357708, 357708);
treeMap.put(878400, 878400);
treeMap.put(845548, 845548);
treeMap.put(833347, 833347);
treeMap.put(90065, 90065);
treeMap.put(735614, 735614);
}
private static void test5() {
TreeMap treeMap = new TreeMap();
// 1. 父節點爲紅,父節點爲右孩子,叔叔節點爲黑,當前節點爲左孩子
treeMap.put(176938, 176938);
treeMap.put(396377, 396377);
treeMap.put(110248, 110248);
treeMap.put(752846, 752846);
treeMap.put(610441, 610441);
}
private static void test6() {
// 1. 父節點爲紅,父節點爲右孩子,叔叔節點爲黑,當前節點爲右孩子
TreeMap treeMap = new TreeMap();
treeMap.put(937433, 937433);
treeMap.put(844023, 844023);
treeMap.put(655944, 655944);
treeMap.put(815559, 815559);
treeMap.put(966114, 966114);
treeMap.put(858760, 858760);
treeMap.put(23926, 23926);
treeMap.put(150570, 150570);
treeMap.put(614739, 614739);
}
}
private void fixAfterInsertion(Entry<K,V> x) {
// 設置當前節點爲紅
x.color = RED;
// 然後判斷是進行左旋,右旋,還是其他操作,只要當前節點不爲空,並且不是根節點,並且父節點爲紅,那麼就考慮旋轉操作
// 進行旋轉操作的前提是對應節點的父節點是紅色
// 這裏是一個循環,然後下面x = parentOf(parentOf(x)); 這個算法是自低向上的
while (x != null && x != root && x.parent.color == RED) {
// 當父節點是祖父的左孩子
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
// 獲取對應的叔叔節點
Entry<K,V> y = rightOf(parentOf(parentOf(x)));
// 叔叔節點爲紅,當前處理節點可左可右
if (colorOf(y) == RED) {
// 場景1:父節點爲紅,父節點爲左孩子,叔叔節點爲紅 --------------------------------------------------------- (只需要調整顏色)
// 父節點染黑
setColor(parentOf(x), BLACK);
// 叔叔節點染黑
setColor(y, BLACK);
// 祖父節點染紅
setColor(parentOf(parentOf(x)), RED);
// x回溯到祖父
x = parentOf(parentOf(x));
} else {
// 叔叔節點爲黑
// X是父節點的右孩子
// 場景2:父節點爲紅,父節點爲左孩,叔叔節點爲黑,當前節點爲右孩子-------------------------- (需要左旋,然後右旋)
if (x == rightOf(parentOf(x))) {
// X指向父節點
x = parentOf(x);
// 左旋父節點
rotateLeft(x);
}
// 場景3:父節點爲紅,父節點爲左孩,叔叔節點爲黑,當前節點爲左孩子-------------------------- (需要直接右旋)
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
// 右旋
rotateRight(parentOf(parentOf(x)));
}
} else {
Entry<K,V> y = leftOf(parentOf(parentOf(x)));
if (colorOf(y) == RED) {
// 場景4:父節點爲紅,父節點爲右孩子,叔叔節點爲紅 --------------------------------------------------------- (只需要調整顏色)
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
// 場景5: 父節點爲紅,父節點爲右孩,叔叔節點爲黑,當前節點爲左孩子-------------------------- (需要右旋,然後左旋)
if (x == leftOf(parentOf(x))) {
x = parentOf(x);
rotateRight(x);
}
// 場景6: 父節點爲紅,父節點爲右孩,叔叔節點爲黑,當前節點爲右孩子-------------------------- (需要直接左旋)
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
rotateLeft(parentOf(parentOf(x)));
}
}
}
// 根節點設置爲黑
root.color = BLACK;
}
remove操作
參考:
https://www.cnblogs.com/cutter-point/p/11587453.html
參考
https://www.cnblogs.com/cutter-point/p/11587453.html
https://www.cnblogs.com/cutter-point/p/10976416.html
https://baike.baidu.com/item/紅黑樹/2413209
https://www.bilibili.com/video/BV1iJ411E7xW?p=125&vd_source=45cea88d5df1dd8adc80a4c6958ab8fd
WeakHashMap
- 強引用(Strong Reference),我們正常編碼時默認的引用類型,強應用之所以爲強,是因爲如果一個對象到GC Roots強引用可到達,就可以阻止GC回收該對象
- 軟引用(Soft Reference)阻止GC回收的能力相對弱一些,如果是軟引用可以到達,那麼這個對象會停留在內存更時間上長一些。當內存不足時垃圾回收器纔會回收這些軟引用可到達的對象
- 弱引用(WeakReference)無法阻止GC回收,如果一個對象時弱引用可到達,那麼在下一個GC回收執行時,該對象就會被回收掉。
- 虛引用(Phantom Reference)十分脆弱,它的唯一作用就是當其指向的對象被回收之後,自己被加入到引用隊列,用作記錄該引用指向的對象已被銷燬