Java原始碼閱讀之TreeMap(紅黑樹) - JDK1.8
閱讀優秀的原始碼是提升程式設計技巧的重要手段之一。
如有不對的地方,歡迎指正~
轉載請註明出處https://blog.lzoro.com。
前言
開門見山,山外有山,山外有山...
先簡單介紹下TreeMap,來看下類關係圖。
怎麼說呢,TreeMap
就是一個有序的鍵值對集合(這介紹有夠簡單的)。
TreeMap實現了NavigableMap
介面, 而NavigableMap
則是通過sortedMap
間接繼承了Map
介面,它定義了一系列導航方法,這些Map之外的方法算是和HashMap
的不同,另外的不同點還在於順序性。
關於TreeMap
和HashMap
的異同點,在接下來的每個章節都可能會提到。
如果還未了解過HashMap
的,可以移步這裡Java原始碼閱讀之HashMap - JDK1.8和這裡Java原始碼閱讀之紅黑樹在HashMap中的應用 - JDK1.8。
接下來,請坐好,準備發車了。
基礎
老規矩,不想上來就整一大堆複雜晦澀的方法,還是先從變數了解起。
成員變數
/**
* comparator用來保持treemap的順序性
* 如果是null,則採取自然順序
*
* @serial
*/
private final Comparator<? super K> comparator;
/**
* 紅黑樹根節點
*/
private transient Entry<K,V> root;
/**
* 鍵值對數量
*/
private transient int size = 0;
/**
* 結構修改次數
*/
private transient int modCount = 0;
從變數可以簡單看出treemap
跟HashMap
有點類似,而不同點在於
- HashMap
1、基於雜湊桶+連結串列/紅黑樹實現
2、無序的 - TreeMap
1、基於紅黑樹實現
2、有序的,通過指定的comparator或者自然順序
接下來看下建構函式
建構函式
/**
* 空參構造,利用自然排序構造一個空的tree map
* 所有的key,必須實現Comparable介面
* 與此同時,所以的key必須具備可比性,{@code k1.compareTo(k2)}不能丟擲{@code ClassCastException}
* 假如你試圖放一個違反約束的key到map裡面,如:放一個string型別的key到原先儲存interger型別key的map裡面,將會丟擲{@code
*/
public TreeMap() {
comparator = null;
}
/**
* 根據給定的comparator構造一個空的/新的map
* 所有插入到map的key通過comparator比較器必須具備可比性
*(因為提供了comparator比較器,所以key可以不用實現Comparable介面)
*
*
* @param comparator comparator如果為null,則使用自然順序
*/
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
/**
* 根據給定個的map和key的自然順序構造一個空的treemap
*
* 關於key的約束同上。
*
* 方法的時間複雜度為n*log(n)
*
* @param m 要放到treemap中的map
* @throws ClassCastException key不具備可比/排序性則拋此異常
* @throws NullPointerException 指定的map是null則拋NPE
*/
public TreeMap(Map<? extends K, ? extends V> m) {
comparator = null;
//呼叫putAll存放m,後續分析
putAll(m);
}
/**
* 根據給定是sortedMap,利用相同的排序方式構造一個新的treemap
*
* 方法以限行時間執行
*
* @param m sortedmap
* @throws NullPointerException 指定的map是null則拋NPE
*/
public TreeMap(SortedMap<K, ? extends V> m) {
//獲取sortedmap的comparator
comparator = m.comparator();
try {
//呼叫buildFromSorted來存放m
buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
} catch (java.io.IOException cannotHappen) {
} catch (ClassNotFoundException cannotHappen) {
}
}
看完了上面幾個建構函式,讓人印象比較深刻的是對於key的約束說明
不指定comparator時,存放到map裡的key必須實現Comparable介面
這裡約束目的就是為了利用可比性來維護treemap的順序性。
上面建構函式中putAll
和buildFromSorted
沒有跟進做具體分析,放置在功能方法裡一併介紹。
紅黑樹
看完變數和建構函式,本來想直接分析功能方法,但是仔細一看,雖然TreeMap
裡紅黑樹的程式碼跟HashMap
本質上是一樣的,但是程式碼的結構還是有較大區別,所以先拿來來賞析。(我覺得TreeMap的紅黑樹程式碼可讀性比HashMap來的高多了)
節點定義
依然是利用一個靜態內部類來定義樹節點,這裡跟HashMap
中的定義類似,還是比較淺顯易懂,不做太多分析。
static final class Entry<K,V> implements Map.Entry<K,V> {
K key;
V value;
Entry<K,V> left;
Entry<K,V> right;
Entry<K,V> parent;
boolean color = BLACK;
/**
* 根據給定的key/value/parent建立一個新的單元節點(黑)
* 子樹為null
*
*/
Entry(K key, V value, Entry<K,V> parent) {
this.key = key;
this.value = value;
this.parent = parent;
}
/**
* 返回key
*
* @return the key
*/
public K getKey() {
return key;
}
/**
* 返回跟key關聯的value
*
* @return the value associated with the key
*/
public V getValue() {
return value;
}
/**
* 替換跟key關聯的value
*
* @return 返回舊值
*/
public V setValue(V value) {
V oldValue = this.value;
this.value = value;
return oldValue;
}
/**
* 比較
*/
public boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
return valEquals(key,e.getKey()) && valEquals(value,e.getValue());
}
/**
* hashcode
*/
public int hashCode() {
int keyHash = (key==null ? 0 : key.hashCode());
int valueHash = (value==null ? 0 : value.hashCode());
return keyHash ^ valueHash;
}
/**
* toString
*/
public String toString() {
return key + "=" + value;
}
}
左旋
這裡的左旋跟HashMap
還是比較相近的,不同點在於HashMap
的入參多了一個root
來用以指向根節點,而在TreeMap
中,root
是一個成員變數。
private void rotateLeft(Entry<K,V> p) {
//null節點忽略
if (p != null) {
//取出p的右子樹
Entry<K,V> r = p.right;
//用r的左子樹替換p的右子樹
p.right = r.left;
//如果r的左子樹存在的話
//則將r的左子樹的父節點指向p
if (r.left != null)
r.left.parent = p;
//r的父節點指向p的父節點
//實質上,就是r替換了p的位置
r.parent = p.parent;
//如果p節點不存在父節點
if (p.parent == null)
//那麼替換了p節點後的r就是根節點
root = r;
else if (p.parent.left == p)
//如果p的父節點存在且p是左子樹
//則將替換p後的r設定為左子樹
p.parent.left = r;
else
//否則設定為右子樹
p.parent.right = r;
//p變成r的左子樹
r.left = p;
//修改引用
p.parent = r;
}
}
右旋
private void rotateRight(Entry<K,V> p) {
//null節點忽略
if (p != null) {
//取出p的左子樹l
Entry<K,V> l = p.left;
//用l的右子樹替換p的左子樹
p.left = l.right;
//如果l人右子樹存在
//則將l的右子樹的父節點指向p
if (l.right != null) l.right.parent = p;
//交換l和p的位置
l.parent = p.parent;
//如果p的父節點不存在
if (p.parent == null)
//那麼替換了p節點後的l就是根節點
root = l;
else if (p.parent.right == p)
//如果p的父節點存在,且p是原右子樹
//則將替換p後的l設定為右子樹
p.parent.right = l;
//否則設定為左子樹
else p.parent.left = l;
//修改引用
l.right = p;
p.parent = l;
}
}
插入平衡
插入平衡方法的實現就是我所說的,我覺得比HashMap
可讀性強的方法。TreeMap
把節點的關係操作封裝成獨立方法了,比如獲取父節點、左子樹、右子樹等,會讓含義很清晰,如果類似於HashMap
是通過引用方式的話,很容易原始碼看著看著就暈乎乎了。
/** From CLR */
private void fixAfterInsertion(Entry<K,V> x) {
//新節點都為紅色
x.color = RED;
//x存在且c不是根節點且x的父節點為紅色
while (x != null && x != root && x.parent.color == RED) {
//如果x的父節點是祖父節點的左子樹的話
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
//取出祖父節點的右子樹
Entry<K,V> y = rightOf(parentOf(parentOf(x)));
//判斷祖父節點右子樹是否為紅色
if (colorOf(y) == RED) {
//紅色
//將父節點變成黑色
setColor(parentOf(x), BLACK);
//祖父節點的右子樹變成黑色
setColor(y, BLACK);
//祖父節點變成紅色
setColor(parentOf(parentOf(x)), RED);
//將x的引用指向祖父節點
x = parentOf(parentOf(x));
} else {
//祖父節點右子樹為黑色
//x節點是父節點的右子樹
if (x == rightOf(parentOf(x))) {
//x引用指向父節點
x = parentOf(x);
//左旋
rotateLeft(x);
}
//將x的父節點變成黑色
setColor(parentOf(x), BLACK);
//x的祖父節點變成紅色
setColor(parentOf(parentOf(x)), RED);
//右旋
rotateRight(parentOf(parentOf(x)));
}
} else {
//如果x的父節點是祖父節點的右子樹的話
//取出祖父節點的左子樹
Entry<K,V> y = leftOf(parentOf(parentOf(x)));
//祖父節點左子樹為紅色
if (colorOf(y) == RED) {
//相關變色操作
setColor(parentOf(x), BLACK);
setColor(y, BLACK);
setColor(parentOf(parentOf(x)), RED);
x = parentOf(parentOf(x));
} else {
//祖父節點左子樹為紅色
//入股x是父節點的左子樹
if (x == leftOf(parentOf(x))) {
x = parentOf(x);
//右旋
rotateRight(x);
}
//相關變色操作
setColor(parentOf(x), BLACK);
setColor(parentOf(parentOf(x)), RED);
//左旋
rotateLeft(parentOf(parentOf(x)));
}
}
}
root.color = BLACK;
}
刪除平衡
刪除平衡也是類似的,程式碼書寫比較規範,為了凸顯我懶,就不新增註釋了,把程式碼貼出來,有緣人自行參悟。
/** From CLR */
private void fixAfterDeletion(Entry<K,V> x) {
while (x != root && colorOf(x) == BLACK) {
if (x == leftOf(parentOf(x))) {
Entry<K,V> sib = rightOf(parentOf(x));
if (colorOf(sib) == RED) {
setColor(sib, BLACK);
setColor(parentOf(x), RED);
rotateLeft(parentOf(x));
sib = rightOf(parentOf(x));
}
if (colorOf(leftOf(sib)) == BLACK &&
colorOf(rightOf(sib)) == BLACK) {
setColor(sib, RED);
x = parentOf(x);
} else {
if (colorOf(rightOf(sib)) == BLACK) {
setColor(leftOf(sib), BLACK);
setColor(sib, RED);
rotateRight(sib);
sib = rightOf(parentOf(x));
}
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(rightOf(sib), BLACK);
rotateLeft(parentOf(x));
x = root;
}
} else { // symmetric
Entry<K,V> sib = leftOf(parentOf(x));
if (colorOf(sib) == RED) {
setColor(sib, BLACK);
setColor(parentOf(x), RED);
rotateRight(parentOf(x));
sib = leftOf(parentOf(x));
}
if (colorOf(rightOf(sib)) == BLACK &&
colorOf(leftOf(sib)) == BLACK) {
setColor(sib, RED);
x = parentOf(x);
} else {
if (colorOf(leftOf(sib)) == BLACK) {
setColor(rightOf(sib), BLACK);
setColor(sib, RED);
rotateLeft(sib);
sib = leftOf(parentOf(x));
}
setColor(sib, colorOf(parentOf(x)));
setColor(parentOf(x), BLACK);
setColor(leftOf(sib), BLACK);
rotateRight(parentOf(x));
x = root;
}
}
}
setColor(x, BLACK);
}
羅列TreeMap
的紅黑樹相關程式碼,是想說明TreeMap
裡面的實現比起HashMap
可讀性更為強一些,但是其實質都是一樣的,所以上面關於插入平衡和刪除平衡的過程這裡不再細說,之前格子的Java原始碼閱讀之紅黑樹在HashMap中的應用 - JDK1.8這篇部落格裡面有過步驟的相關描述,也有一些圖解,有興趣的可以瞭解一下。
功能方法
接下來看下相關功能方法,看下我們平時所使用的方法內部是怎麼實現的。
put
將指定的鍵值對存放到TreeMap
,不同於HashMap
將元素通過HashCode分散到雜湊桶裡面,TreeMap
是通過比較器/自然順序的形式將元素存放到紅黑樹中來保證有序性。
下面開始分析put方法。
/**
* 存放指定的鍵值對
* 如果指定的key存在,舊的value將會被新的替換
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
*
* @return 舊的value值{@code key}, 如果之前不存在,則返回null
* (返回null也有可能key對應的值是null)
* @throws ClassCastException 指定的key不具備可比性的話則拋此異常
* @throws NullPointerException 使用自然排序時指定的key為null/comparator不允許null的key,則拋NPE
*
*/
public V put(K key, V value) {
//根節點
Entry<K,V> t = root;
//如果根節點還不存在(TreeMap是空的)
if (t == null) {
//這裡的比較做一個型別檢查
//可能null
compare(key, key); // type (and possibly null) check
//初始化一個節點
root = new Entry<>(key, value, null);
//size + 1
size = 1;
//修改計數 + 1
modCount++;
//返回null
return null;
}
//如果TreeMap不為空
//定義比較值
int cmp;
//定義父節點
Entry<K,V> parent;
// 分離Comparator和比較路徑
Comparator<? super K> cpr = comparator;
//如果存在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 {
//自然順序比較不允許key為null
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 + 1
size++;
//修改計數 + 1
modCount++;
//返回null(執行到這一步,證明未找到相同的key,如果有,則在上面就return了)
return null;
}
看完了put的,再把之前建構函式中的未加分析的putAll
一併閱讀(完全無違和感)。
/**
* 將指定map中的元素都存放到當前treemap
*
* @param map map
* @throws ClassCastException key不合法(參照建構函式章節)
* @throws NullPointerException 指定的map為null/或者存在null的key且treemap不允許null-key的情況下丟擲NPE
*/
public void putAll(Map<? extends K, ? extends V> map) {
//獲取大小
int mapSize = map.size();
//treemap為空且指定的map不為空並且map是可排序的map
if (size==0 && mapSize!=0 && map instanceof SortedMap) {
//獲取Comparator
Comparator<?> c = ((SortedMap<?,?>)map).comparator();
//判斷Comparator是否跟當前的一致
if (c == comparator || (c != null && c.equals(comparator))) {
//操作計數 + 1
++modCount;
try {
//呼叫buildFromSorted進行處理
buildFromSorted(mapSize, map.entrySet().iterator(),
null, null);
} catch (java.io.IOException cannotHappen) {
} catch (ClassNotFoundException cannotHappen) {
}
return;
}
}
//呼叫父類的putAll
super.putAll(map);
}
通過分析以上的程式碼,可以看出putAll
裡面的邏輯還是比較簡單的,一是判斷當前treemap是否為空,且給定map的大小合法,並且是給定的map是SortedMap
的例項if (size==0 && mapSize!=0 && map instanceof SortedMap)
。
如果是,則取出比較器判斷後呼叫buildFromSorted
進行處理
如果不是,則呼叫父類的putAll
進行處理。
這裡留有兩個疑問,buildFromSorted
和父類的putAll
究竟做了哪些處理來完成集合元素的存放呢?
下面一步步分析,先從父類的putAll看起 。
/**
* {@inheritDoc}
* 嗯,懶得翻譯了,反正我翻譯水平也比較差。
*
* @implSpec
* This implementation iterates over the specified map's
* <tt>entrySet()</tt> collection, and calls this map's <tt>put</tt>
* operation once for each entry returned by the iteration.
*
* <p>Note that this implementation throws an
* <tt>UnsupportedOperationException</tt> if this map does not support
* the <tt>put</tt> operation and the specified map is nonempty.
*
* @throws UnsupportedOperationException {@inheritDoc}
* @throws ClassCastException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
* @throws IllegalArgumentException {@inheritDoc}
*/
public void putAll(Map<? extends K, ? extends V> m) {
for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
put(e.getKey(), e.getValue());
}
是不是一目瞭然了。如果上面提到的判斷if (size==0 && mapSize!=0 && map instanceof SortedMap)
不成立,則呼叫父類的putAll
方法:通過迴圈,將元素一個個放到treemap當中,這裡的放置put
就是在本章節開頭分析的put
方法。
那麼,還剩下一個疑問,如果上面的判斷成立,buildFromSorted
又做了哪些操作呢?
/**
*
* 線性時間的樹構造演算法(根據排序資料)
* 可以從迭代器/流當中接受鍵值對
* 有很多方法入參,但是似乎還是比其他選擇更好(PS:我也不知道其他選擇是什麼)
*
* 該方法接受的4種格式說明:
*
* 1) Map.Entries迭代器. (it != null, defaultVal == null).
* 2) key的迭代器. (it != null, defaultVal != null).
* 3) 交替序列化的鍵值對流.(it == null, defaultVal == null).
* 4) 序列化的鍵流. (it == null, defaultVal != null).
*
* 假設呼叫此方法前comparator已經被設定
*
* @param size 鍵/或者鍵值對的數量
* @param it 不為null的話, 新的entries通過這個迭代器建立
* @param str 不為null的話, 新的entries通過序列化流來建立
* 準確點說,it和str必須一個不為null
* @param defaultVal 不為null的話, 會作為預設值
* @throws java.io.IOException 讀取流時可能會丟擲NPE,如果str為null則不會發生這種情況
* @throws ClassNotFoundException 讀取物件是可能拋此異常.如果str為null則不會發生這種情況
*/
private void buildFromSorted(int size, Iterator<?> it,
java.io.ObjectInputStream str,
V defaultVal)
throws java.io.IOException, ClassNotFoundException {
//設定size
this.size = size;
//呼叫buildFromSorted來確定root
//分割線之後繼續分析
//這裡有個小插曲computeRedLevel
//computeRedLevel是根據節點數量來計算完全二叉樹的層級
//其實從名字看來,可以理解為計算紅色節點的層級
root = buildFromSorted(0, 0, size-1, computeRedLevel(size),
it, str, defaultVal);
}
---------------------------------------------------
/**
* 計算紅色節點所在層級
* (完全二叉樹的層級)
* 從0開始
*
*/
private static int computeRedLevel(int sz) {
int level = 0;
for (int m = sz - 1; m >= 0; m = m / 2 - 1)
level++;
return level;
}
---------------------------------------------------
//個是buildFromSorted的實際實現方法
/**
* 遞迴的、真正的實現方法(之前是幫助方法).
* 跟之前的方法比較,相同的引數命名具有相同的意義
* 增加的引數說明在下方
*
* 假定在呼叫此方法之前已經設定了樹圖的比較器和大小欄位。(它忽略了這兩個欄位)。
*
* @param level 當前樹的層級. 初始化呼叫應該為0.
* @param lo 子樹的首個節點索引. 初始化應該為0.
* @param hi 子樹的尾節點索引. 初始化應該為size - 1
* @param redLevel 節點該為紅色的層級,必須以size和computeRedLevel計算出來的相等
*/
@SuppressWarnings("unchecked")
private final Entry<K,V> buildFromSorted(int level, int lo, int hi,
int redLevel,
Iterator<?> it,
java.io.ObjectInputStream str,
V defaultVal)
throws java.io.IOException, ClassNotFoundException {
/*
* 策略: 根節點是最接近中間節點的元素. 為了得到它,首先我們必須遞迴構建完整的左子樹,以便抓取所有的元素
* 然後我們可以繼續處理右子樹
*
* lo和li引數是為當前子樹提取迭代器/流的最小和最大指標,
* 它們實際上沒有索引,我們只是按順序處理,確保items被按相應的順序處理。
*
*/
//如果hi小於lo,
if (hi < lo) return null;
//mid=(lo+hi)/2; - 無符號右移
int mid = (lo + hi) >>> 1;
//左子樹
Entry<K,V> left = null;
//如果lo小於mid
//遞迴構造左子樹
if (lo < mid)
left = buildFromSorted(level+1, lo, mid - 1, redLevel,
it, str, defaultVal);
// extract key and/or value from iterator or stream
//從迭代器/流中獲取鍵值對
K key;
V value;
//使用迭代器
if (it != null) {
//沒有有預設值
if (defaultVal==null) {
Map.Entry<?,?> entry = (Map.Entry<?,?>)it.next();
key = (K)entry.getKey();
value = (V)entry.getValue();
} else {
//有預設值
key = (K)it.next();
value = defaultVal;
}
} else { // use stream
//使用流
key = (K) str.readObject();
value = (defaultVal != null ? defaultVal : (V) str.readObject());
}
//建立節點
Entry<K,V> middle = new Entry<>(key, value, null);
// color nodes in non-full bottommost level red
//非null節點且是紅色層級的,染色成紅色
if (level == redLevel)
middle.color = RED;
//判斷左子樹是否為null
if (left != null) {
//指向左子樹
middle.left = left;
//修改引用
left.parent = middle;
}
//遞迴構造右子樹
if (mid < hi) {
Entry<K,V> right = buildFromSorted(level+1, mid+1, hi, redLevel,
it, str, defaultVal);
middle.right = right;
right.parent = middle;
}
//到遞迴的最外層的話這裡的middle就是最終的根節點
return middle;
}
到這裡,關於TreeMap
的put
相關方法就分析完畢了,有幾個要點梳理一下
- 1、
put
方法根據比較器/自然順序將元素放置到紅黑樹特定位置後,進行插入平衡 - 2、
putAll
實際上有兩種情況,一個是迭代取出元素呼叫父類的put
,另外是呼叫buildFromSorted
完成TreeMap構造 - 3、呼叫
buildFromSorted
的前提是,入參必須是SortedMap
的例項(還有其他限制,詳見上面的if條件) - 4、buildFromSorted裡面有一個
computeRedLevel
,是用來計算紅色節點層級(也可以理解為計算完全二叉樹層級) - 5、實際實現
buildFromSorted
的方法,是一個遞迴呼叫的過程,通過middle,遞迴構造左右子樹來完成整棵樹的構建。
Go On,下面是remove的方法。
remove
/**
* 如果存在的話,根據指定的key從treemap中移除指定的鍵值對
*
* @param key 要移除的鍵值對的key
* @return 和{@code key}相關聯的舊值
* 如果{@code key}.沒有對映的話為{@code null},返回null的時候也有可能和key相關聯的是null
* @throws ClassCastException 指定的key無法和map中的key進行比較,則拋此異常
* @throws NullPointerException 指定的key是null且該treemap採取自然排序/comparator不允許null的key時,拋NPE
*/
public V remove(Object key) {
//根據key獲取指定元素節點
Entry<K,V> p = getEntry(key);
//為null則返回
if (p == null)
return null;
//取出舊節點的值
V oldValue = p.value;
//刪除元素
deleteEntry(p);
//返回舊值
return oldValue;
}
從原始碼可以看出,remove
方法體裡面有兩個關鍵呼叫,getEntry
和deleteEntry
,深入瞭解一下。
/**
* 根據給定給定key,返回元素,如果沒存在,則返回null
*
* @throws ClassCastException 指定的key無法與map中的比較時,丟擲此異常
* @throws NullPointerException 指定的key是null且該treemap採取自然排序/comparator不允許null的key時,拋NPE
*/
final Entry<K,V> getEntry(Object key) {
// Offload comparator-based version for sake of performance
// 判斷是否存在comparator
if (comparator != null)
//如果comparator存在的話,呼叫getEntryUsingComparator
return getEntryUsingComparator(key);
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
Entry<K,V> p = root;
//利用二叉樹性質,進行迴圈搜尋
while (p != null) {
//自然比較
int cmp = k.compareTo(p.key);
//根據比較結果,決定取左子樹還是右子樹
if (cmp < 0)
p = p.left;
else if (cmp > 0)
p = p.right;
else
//如果比較結果相等,則返回該元素
return p;
}
return null;
}
//繼續看getEntryUsingComparator方法
/**
* 通過comparator獲取元素的版本.從genEntry分離出來(整潔美觀效能beautiful~)
* (對於大多數方法來說它是不值得的,因為大多數方法較少依賴於比較器效能,但是在這裡它就是醬紫的呀,它是值得的)
*/
final Entry<K,V> getEntryUsingComparator(Object key) {
@SuppressWarnings("unchecked")
K k = (K) key;
//取出比較器
Comparator<? super K> cpr = comparator;
if (cpr != null) {
Entry<K,V> p = root;
//從根節點迴圈
while (p != null) {
//通過比較器獲取比較結果
int cmp = cpr.compare(k, p.key);
//根據比較結果,決定取左子樹還是右子樹
//嗯,其他的就跟上面自然順序處理是一樣樣兒的
if (cmp < 0)
p = p.left;
else if (cmp > 0)
p = p.right;
else
return p;
}
}
return null;
}
查詢元素的方法還是比較簡單易懂的,但是不能漏掉deleteEntry
這個查詢後刪除的方法,其實這裡的deleteEntry
就是紅黑樹的節點刪除操作了,之前也貌似也分析過,這裡還是把程式碼和註釋貼來,也許你跟我一樣也是小懶蛋呢(科普一下:優秀的懶人會有創新的,因為不想重複勞動)
/**
* 刪除p節點,然後處理刪除平衡
*/
private void deleteEntry(Entry<K,V> p) {
//首先,現實相關計數處理
modCount++;
size--;
// If strictly internal, copy successor's element to p and then make p
// point to successor.
//內部嚴謹的話,拷貝p的後置節點給p,然後將p指向後置節點
//左右子樹都存在的情況
if (p.left != null && p.right != null) {
//獲取後置節點
Entry<K,V> s = successor(p);
//後置節點的相關值賦值給p
p.key = s.key;
p.value = s.value;
//p指向s
p = s;
} // p has 2 children
// Start fixup at replacement node, if it exists.
//開始在替換節點進行修正
//如果p的左右子樹都存在一個的話,則p在上面的條件分支裡已經指向s了
//取出替換節點
Entry<K,V> replacement = (p.left != null ? p.left : p.right);
//判斷替換節點是否存在
if (replacement != null) {
// Link replacement to parent
//修改父節點引用
replacement.parent = p.parent;
//如果p不存在父節點,那麼替換p的replacement節點就是根節點了
//很好,登基了(朕一日不死,你們就都是太子)
if (p.parent == null)
root = replacement;
//如果p是父節點的左子樹
else if (p == p.parent.left)
//那麼修改父節點的左子樹引用為新的替換節點
p.parent.left = replacement;
else
//否則修改右子樹引用
p.parent.right = replacement;
// Null out links so they are OK to use by fixAfterDeletion.
//p節點的相關引用置為null,以便後面的刪除平衡處理
p.left = p.right = p.parent = null;
// Fix replacement
//如果p節點是黑色節點的話,則進行刪除平衡
if (p.color == BLACK)
fixAfterDeletion(replacement);//這個方法在開頭的紅黑樹說明有,或者可以參考我的另外一篇hashmap紅黑樹部落格
//如果替換節點不存在,且p的父節點也不存在
} else if (p.parent == null) { // return if we are the only node.
//則證明p的唯一的節點,返回null
root = null;
} else { // No children. Use self as phantom replacement and unlink.
//p有父節點,但是沒有子節點了
//判斷p的顏色是否為黑
if (p.color == BLACK)
//如果是,進行刪除平衡
fixAfterDeletion(p);
//p的父節點存在
//判斷p是父節點的左子樹還是右子樹
//並進行相關引用修改
if (p.parent != null) {
if (p == p.parent.left)
p.parent.left = null;
else if (p == p.parent.right)
p.parent.right = null;
p.parent = null;
}
}
}
//獲取後置節點
/**
* 返回後置節點如果存在的話,如果不存在,返回null
*/
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
//t為null,則直接返回null
if (t == null)
return null;
//右子樹存在
else if (t.right != null) {
//取出右子樹
Entry<K,V> p = t.right;
//迴圈,遍歷並迴圈取左子樹,取出最後一個
while (p.left != null)
p = p.left;
return p;
} else {
//左子樹存在
//取出父節點
Entry<K,V> p = t.parent;
//ch指向t
Entry<K,V> ch = t;
//現在的ch(t)是p的子樹
//迴圈(只要父節點存在,且ch(t)節點是父節點的右子樹的話)
while (p != null && ch == p.right) {
ch = p;
p = p.parent;
}
return p;
}
}
//可以看出來,取出後置節點是這麼處理的:
1、如果t的右子樹存在的話,就一路向左下遍歷,直到null
2、如果t的左子樹存在的話,就一路向向上遍歷(t必須是父節點的右子樹),直到不符合情況
get
(⊙o⊙)…
如果仔細看了remove章節的話,其實這個章節可以略過了。
因為get屬於門面方法,實際實現也是由getEntry
提供的。
public V get(Object key) {
Entry<K,V> p = getEntry(key);
return (p==null ? null : p.value);
}
containsKey/containsValue
判斷TreeMap
是否存在對應的key或者對應的value。
判斷key比較簡單,跟上面get
章節是相同道理的,根據key去獲取元素,並判斷元素是否為null。
判斷value跟判斷key不一樣,但是邏輯也很清晰,首先取出首個元素,然後迴圈迭代,用指定value和每個元素的value做比較,相同則返回。
public boolean containsKey(Object key) {
return getEntry(key) != null;
}
--------------------------------------------------
public boolean containsValue(Object value) {
//迭代判斷
for (Entry<K,V> e = getFirstEntry(); e != null; e = successor(e))
if (valEquals(value, e.value))
return true;
return false;
}
forEach
迴圈迭代,並對每個元素做指定的操作(action)。
這裡的迴圈迭代跟上面的containsValue
是一樣的,不通點在於containsValue
是對每個元素執行判斷,而forEach
是對每個元素執行相應的action。
@Override
public void forEach(BiConsumer<? super K, ? super V> action) {
Objects.requireNonNull(action);
int expectedModCount = modCount;
for (Entry<K, V> e = getFirstEntry(); e != null; e = successor(e)) {
action.accept(e.key, e.value);
if (expectedModCount != modCount) {
throw new ConcurrentModificationException();
}
}
}
entrySet
本來不打算把這個拿出來分析的,因為完整的分析篇幅實在是太長了。
但是既然都這麼長了,還在乎差這一截嗎~
這個方法我們用的也是相對比較頻繁的,單看entrySet
方法根本沒什麼好看的,很簡單,內部有一個entrySet變數,如果未初始化,則new一個,如果已初始化,則返回。
public Set<Map.Entry<K,V>> entrySet() {
EntrySet es = entrySet;
return (es != null) ? es : (entrySet = new EntrySet());
}
來看一下EntrySet
的資料結構,它是TreeMap
的內部類,並且繼承了AbstractSet
,並實現了相關方法,用過Set
的小夥伴應該相熟悉。
// TreeMap.java 1057行
//Entry定義
class EntrySet extends AbstractSet<Map.Entry<K,V>> {
/**
* 返回迭代器
*/
public Iterator<Map.Entry<K,V>> iterator() {
//這裡呼叫的getFirstEntry方法,之前分析過
//獲取了首節點元素後,建立一個EntryIterator
//這裡迭代器相關程式碼不貼了,有興趣的可以自行了解
//TreeMap 1238行
return new EntryIterator(getFirstEntry());
}
/**
* 判斷是否包含元素o
*/
public boolean contains(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> entry = (Map.Entry<?,?>) o;
Object value = entry.getValue();
Entry<K,V> p = getEntry(entry.getKey());
return p != null && valEquals(p.getValue(), value);
}
/**
* 移除元素o
*/
public boolean remove(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> entry = (Map.Entry<?,?>) o;
Object value = entry.getValue();
Entry<K,V> p = getEntry(entry.getKey());
if (p != null && valEquals(p.getValue(), value)) {
deleteEntry(p);
return true;
}
return false;
}
public int size() {
return TreeMap.this.size();
}
public void clear() {
TreeMap.this.clear();
}
public Spliterator<Map.Entry<K,V>> spliterator() {
return new EntrySpliterator<K,V>(TreeMap.this, null, null, 0, -1, 0);
}
}
單從上面的原始碼看來,entrySet
其實也沒什麼好分析的,不過Set
裡面還是有很多方法在平時會用到的,之後找個時間,專門開一篇分析Set
好了。
天色已晚,各位小夥伴下車洗洗睡吧。
總結
嗯,沒有總結,都在上面了。
溜了溜了。給個讚唄。
相關文章
- 死磕 java集合之TreeMap原始碼分析(四)——紅黑樹全解析Java原始碼
- 死磕 java集合之TreeMap原始碼分析(二)——紅黑樹全解析Java原始碼
- 死磕 java集合之TreeMap原始碼分析(三)——紅黑樹全解析Java原始碼
- 死磕 java集合之TreeMap原始碼分析(一)——紅黑樹全解析Java原始碼
- 二、JAVA知識點之HashMap、TreeMap、紅黑樹——精髓JavaHashMap
- 死磕 java集合之TreeMap原始碼分析(二)- 內含紅黑樹分析全過程Java原始碼
- 死磕 java集合之TreeMap原始碼分析(三)- 內含紅黑樹分析全過程Java原始碼
- 死磕 java集合之TreeMap原始碼分析(一)- 內含紅黑樹分析全過程Java原始碼
- 徹底理解紅黑樹及JavaJDK1.8TreeMap原始碼分析JavaJDK原始碼
- Java集合(3)一 紅黑樹、TreeMap與TreeSet(上)Java
- Java集合(4)一 紅黑樹、TreeMap與TreeSet(下)Java
- JDK1.8原始碼(十一)——java.util.TreeMap類JDK原始碼Java
- Java集合原始碼分析之基礎(六):紅黑樹(RB Tree)Java原始碼
- JDK1.8 ConcurrentHashMap原始碼閱讀JDKHashMap原始碼
- JDK1.8 原始碼分析(十) -- TreeMapJDK原始碼
- JDK1.8原始碼分析03之idea搭建原始碼閱讀環境JDK原始碼Idea
- String(JDK1.8)原始碼閱讀記錄JDK原始碼
- 紅黑樹核心程式碼分析(JAVA)Java
- 【原始碼閱讀】Glide原始碼閱讀之with方法(一)原始碼IDE
- 【原始碼閱讀】Glide原始碼閱讀之into方法(三)原始碼IDE
- 【原始碼閱讀】Glide原始碼閱讀之load方法(二)原始碼IDE
- JDK1.8原始碼閱讀筆記(1)Object類JDK原始碼筆記Object
- 原始碼閱讀之Java棧的實現原始碼Java
- 如何閱讀Java原始碼?Java原始碼
- jdk1.8原始碼解析:HashMap底層資料結構之連結串列轉紅黑樹的具體時機JDK原始碼HashMap資料結構
- 資料結構與演算法(十四)深入理解紅黑樹和JDK TreeMap和TreeSet原始碼分析資料結構演算法JDK原始碼
- 【原始碼閱讀】AndPermission原始碼閱讀原始碼
- Qt原始碼閱讀(三) 物件樹管理QT原始碼物件
- jdk原始碼分析之TreeMapJDK原始碼
- java 8 HashMap 原始碼閱讀JavaHashMap原始碼
- 演算法導論學習--紅黑樹詳解之刪除(含完整紅黑樹程式碼)演算法
- 紅黑樹
- java基礎:TreeMap — 原始碼分析Java原始碼
- Java集合原始碼分析(十四):TreeMapJava原始碼
- Java基礎-理解紅黑樹(插入)Java
- 資料結構之「紅黑樹」資料結構
- Java原始碼跟蹤閱讀技巧Java原始碼
- Java類載入原始碼閱讀Java原始碼