深入理解Java集合之Map
Map筆錄
Map 提供了一個更通用的元素儲存方法。 Map 集合類用於儲存元素對(稱作“鍵”和“值”),其中每個鍵對映到一個值。標準的Java類庫中包含了Map的幾種基本實現,包括HashMap、TreeMap、LinkedHashMap、WeakHashMap、ConcurrentHashMap、IdentityHashMap。它們都有相同的介面Map、但是行為特性各不相同,這表現在效率、鍵值對的儲存以及呈現次序、物件的儲存週期、對映表如何在多執行緒程式中工作和判定“鍵”等價的策略等方面。
HashMap
JDK1.8之前HashMap是一個連結串列雜湊的資料結構,也就是採用了鏈地址法,利用連結串列來解決雜湊表的衝突,JDK1.8之後又多加了一個紅黑樹,也就是說是桶位(bucket)+連結串列+紅黑樹來實現HashMap,當連結串列長度超過閥值8時,將連結串列轉化為紅黑樹,這樣大大減少了查詢的時間。
HashMap執行緒不安全,效率高。
底層實現的原理:
首先有一個數組,儲存的是連結串列的表頭元素,當新增玉一個元素(key-value)時首先計算key的hash值,以此來確定插入的陣列位置,在計算hash值時,會產生衝突,也就是說可能存在同一hash值的元素已經被放在陣列的同一位置,這時候就要新增到同一hash值的後面,也就是用連結串列的方式儲存,同一連結串列的hash值是相同的,而放連結串列長度太長時,連結串列就會轉換成紅黑樹。以下就是HashMap的原理圖:
定義:
public class HashMap<k,v> extends AbstractMap<k,v> implements Map<k,v>, Cloneable, Serializable
重要屬性:
private static final long serialVersionUID = 362498820763181265L; static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16,預設的初始容量,必須是2的冪。 static final int MAXIMUM_CAPACITY = 1 << 30;// 最大容量(必須是2的冪且小於2的30次方,傳入容量過大將被這個值替換) static final float DEFAULT_LOAD_FACTOR = 0.75f;// 填充比 // 當add一個元素到某個位桶,其連結串列長度達到8時將連結串列轉換為紅黑樹 static final int TREEIFY_THRESHOLD = 8; static final int UNTREEIFY_THRESHOLD = 6; static final int MIN_TREEIFY_CAPACITY = 64; transient Node<k, v>[] table;// 儲存元素的陣列 transient Set<map.entry<k, v>> entrySet; transient int size;// 存放元素的個數 transient int modCount;// 被修改的次數fast-fail機制 int threshold;// 臨界值 當實際大小(容量*填充比)超過臨界值時,會進行擴容 final float loadFactor;// 填充比(......後面略)
以下是Node<K,V>的原始碼實現
static class Node<K, V> implements Map.Entry<K, V> {
final int hash;
final K key;
V value;
Node<K, V> next;
Node(int hash, K key, V value, Node<K, V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() {
return key;
}
public final V getValue() {
return value;
}
public final String toString() {
return key + "=" + value;
}
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?, ?> e = (Map.Entry<?, ?>) o;
if (Objects.equals(key, e.getKey())
&& Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
構造方法:
//建構函式1
//根據給定的初始容量的裝載因子建立一個空的HashMap
//初始容量小於0或裝載因子小於等於0將報異常
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);//新的擴容臨界值
}
//建構函式2
//根據指定容量建立一個空的HashMap
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//建構函式3
//使用預設的容量及裝載因子構造一個空的HashMap
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
//建構函式4用m的元素初始化雜湊對映
public HashMap(Map<!--? extends K, ? extends V--> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
到此處,肯定有很多名次不解,比如裝載因子等,下面就一一解釋:
負載因子:為了確定合適調整大小,而不是對每個儲存桶中的連結串列的深度做計數,基於雜湊的Map使用一個額外的引數並粗略計算儲存桶的密度。Map在調整大小之前,使用名為“裝載因子”的引數指示Map將承擔的負載量,即負載程度。裝載因子、項數(Map大小)與容量之間的關係是:若裝載因子*容量>map大小,則調整map大小。例如如果預設負載因子為0.75,容量為11,則11*0.75=8.25,則值向下取整為8個元素,因此當第8個項新增到此map,則Map會將自身的大小調整為一個更大的值。HashMap本來就是以空間換時間,所以裝載因子沒必要太大,但是太小又會導致空間的浪費,若關注的是記憶體,則裝載因子可以稍大,若追求的是效能,則應該稍微小一點。
獲取資料:
public V get(Object key) {
Node<K, V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
* Implements Map.get and related methods
*
* @param hash
* hash for key
* @param key
* the key
* @return the node, or null if none
*/
final Node<K, V> getNode(int hash, Object key) {
Node<K, V>[] tab;// Entry物件陣列
Node<K, V> first, e; // 在tab陣列中經過雜湊的第一個位置
int n;
K k;
/* 找到插入的第一個Node,方法是hash值和n-1相與,tab[(n - 1) & hash] */
// 也就是說在一條鏈上的hash值相同的
if ((tab = table) != null && (n = tab.length) > 0
&& (first = tab[(n - 1) & hash]) != null) {
/* 檢查第一個Node是不是要找的Node */
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))// 判斷條件是hash值要相同,key值要相同
return first;
/* 檢查first後面的node */
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K, V>) first).getTreeNode(hash, key);
/* 遍歷後面的連結串列,找到key值和hash值都相同的Node */
do {
if (e.hash == hash
&& ((k = e.key) == key || (key != null && key
.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
使用get(key)方法時獲取key的hash值,計算hash&(n-1)得到連結串列陣列中的位置first=tab[hash&(n-1)],先判斷first的值是否與引數key相等,不等就遍歷後面的連結串列,利用equals()來找到相同的key值返回物件的value值。
儲存資料:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
* Implements Map.put and related methods
*
* @param hash
* hash for key
* @param key
* the key
* @param value
* the value to put
* @param onlyIfAbsent
* if true, don't change existing value
* @param evict
* if false, the table is in creation mode.
* @return previous value, or null if none
*/
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;
/*如果table的在(n-1)&hash的值是空,就新建一個節點插入在該位置*/
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
/*表示有衝突,開始處理衝突*/
else {
Node<K,V> e;K k;
/*檢查第一個Node,p是不是要找的值*/
if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {
/*指標為空就掛在後面*/
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//如果衝突的節點數已經達到8個,看是否需要改變衝突節點的儲存結構,
//treeifyBin首先判斷當前hashMap的長度,如果不足64,只進行
//resize,擴容table,如果達到64,那麼將衝突的儲存結構為紅黑樹
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
/*如果有相同的key值就結束遍歷*/
if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
/*就是連結串列上有相同的key值*/
if (e != null) { // existing mapping for key,就是key的Value存在
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;//返回存在的Value值
}
}
++modCount;
/*如果當前大小大於門限,門限原本是初始容量*0.75*/
if (++size > threshold)
resize();//擴容兩倍
afterNodeInsertion(evict);
return null;
}
put(key,value)的儲存過程:1.判斷鍵值對陣列tab[]是否為空或為null,否則以預設大小resize()。2.根據key值來計算hash值,得到插入陣列的位置,若tab[i]==null,則直接建立節點新增,若不為空,則起衝突。3.判斷當前陣列中處理衝突的方式為連結串列還是紅黑樹,分別處理。
現在來講講涉及到的HashMap擴容機制:構造hash表時,如果不指明初始大小,預設大小為16,如果Node[]陣列達到(裝載因子*Node.length),則重新調整HashMap大小為原來大小的兩倍。
/**
* Initializes or doubles table size. If null, allocates in
* accord with initial capacity target held in field threshold.
* Otherwise, because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move
* with a power of two offset in the new table.
*
* @return the table
*/
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;
}
//把新表的長度設定為舊錶長度的兩倍,newCap=2*oldCap
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//把新表的門限設定為舊錶門限的兩倍,newThr=oldThr*2
newThr = oldThr << 1; // double threshold
}
//如果舊錶的長度的是0,就是說第一次初始化表
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
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;//把新表賦值給table
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)//說明這個node沒有連結串列直接放在新表的e.hash & (newCap - 1)位置
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//如果e後邊有連結串列,到這裡表示e後面帶著個單鏈表,需要遍歷單鏈表,將每個結點重
else { // preserve order保證順序
//新計算在新表的位置,並進行搬運
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;//記錄下一個結點
//新表是舊錶的兩倍容量,例項上就把單鏈表拆分為兩隊,
//e.hash&oldCap為偶數一隊,e.hash&oldCap為奇數一對
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);
//lo隊不為null,放在新表原位置
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//hi隊不為null,放在新表j+oldCap位置
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
JDK1.8中處理衝突增加了紅黑樹,當衝突較少時採用連結串列來解決衝突,當較大時(>8個時)採用紅黑樹(從查詢時間O(n)優化到了O(logn))儲存。
而上述的get()以及put()方法都應用到了hashCode()跟equals(),這兩個方法是HashMap中應用廣泛的方法,並且我們在用物件進行作為鍵的時候,必須要重寫hashCode(),equals()。先來看看原始碼:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
從這裡看出,若沒有重寫equals()跟hashCode(),Java類庫中預設的是使用Objects的equals()跟hashCode()方法。這樣在實際的使用中很容易出錯。例如:
寫一個user
package crazyHzm;
public class User {
int id;
User(int id) {
this.id = id;
}
public String toString() {
return "key="+id;
}
}
下面寫一個測試類:
package crazyHzm;
import java.util.HashMap;
import java.util.Map;
public class TestMap {
public static void main(String[] args) {
Map map = new HashMap();
map.put(new User(1), 1);
map.put(new User(2), 1);
map.put(new User(3), 1);
System.out.println(map.keySet());
System.out.println(map.get(new User(1)));
}
}
檢視結果:
嘗試在User類裡面重寫這兩個方法:
package crazyHzm;
public class User {
int id;
User(int id) {
this.id = id;
}
public String toString() {
return "key="+id;
}
public int hashCode() {
return id;
}
public boolean equals(Object o) {
return o instanceof User && (id == ((User) o).id);
}
}
檢視結果:
這個時候就能夠在Map裡面找到了,所以equals()相等的物件,hashCode()的值一定相等,但是equals()不相等的兩個物件,他們的hashCode()並不一定相等,這是雜湊表衝突造成的,反過來hashCode()不相等的,equals()一定不相等,但是hashCode()相等的,equals()不一定相等。
TreeMap
TreeMap基於紅黑樹,檢視“鍵”或“鍵值對”時,它們會被排序(次序由Comparable或者Comparator決定)。TreeMap特點在於,所得到的結果是經過排序的。TreeMap是 唯一帶有subMap()方法的Map,它可以返回一個子樹。
底層原理:
樹節點Entry實現了Map.Entry,採用內部類實現:
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;
/**
* Make a new cell with given key, value, and parent, and with
* {@code null} child links, and BLACK color.
*/
Entry(K key, V value, Entry<K,V> parent) {
this.key = key;
this.value = value;
this.parent = parent;
}
定義:
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
屬性:
// 用於接收外部比較器,插入時用於比對元素的大小
private final Comparator<? super K> comparator;
// 紅黑樹的根節點
private transient Entry<K,V> root;
/**
* The number of entries in the tree
*/
// 樹中元素個數
private transient int size = 0;
/**
* The number of structural modifications to the tree.
*/
private transient int modCount = 0;
構造方法:
//預設構造方法會建立一顆空樹。
//預設使用key的自然順序來構建有序樹
//所謂自然順序,意思是key的型別是什麼,就採用該型別的compareTo方法來比較大小,決定順序。
//例如key為String型別,就會用String類的compareTo方法比對大小,如果是Integer型別,就用Integer的compareTo方法比對。
//Java自帶的基本資料型別及其裝箱型別都實現了Comparable介面的compareTo方法。
//key的型別,必須實現Comparable介面,如果不實現,就沒辦法完成元素大小的比較來實現有序性的。
//比如自定義了一個類User來作為key,忘記實現了Comparable介面,就沒有一個規則比較User的大小,無法實現TreeMap最重要的有序性。
public TreeMap() {
comparator = null;
}
//// 提供指定的比較器
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
// 構造方法三,採用自然序維持TreeMap中節點的順序,同時將傳入的Map中的內容新增到TreeMap中
public TreeMap(Map<? extends K, ? extends V> m) {
comparator = null;
putAll(m);
}
//構造方法四,接收SortedMap引數,根據SortedMap的比較器維持TreeMap中的節點順序,
//同時通過buildFromSorted(int size, Iterator it, java.io.ObjectInputStream str, V defaultVal)方法將SortedMap中的內容新增到TreeMap中
public TreeMap(SortedMap<K, ? extends V> m) {
comparator = m.comparator();
try {
buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
} catch (java.io.IOException cannotHappen) {
} catch (ClassNotFoundException cannotHappen) {
}
}
儲存資料:
public V put(K key, V value) {
Entry<K, V> t = root;
if (t == null) {
compare(key, key); // type (and possibly null) check
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
return t.setValue(value);
} while (t != null);
} else {// 預設key的比較器
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;
}
TreeMap的put方法跟其他Map的put方法一樣,向Map中加入鍵值對,若原先的鍵已經存在,那麼替換它的value,並返回原先的值。不過多的是在put後面加入fixAfterInsertion()方法,這個方法負責在插入節點以後調整樹結構和著色,以滿足紅黑樹的要求。以下就是這個方法的原始碼:
private void fixAfterInsertion(Entry<K, V> x) {
// 插入節點預設為紅色
x.color = RED;
// 迴圈條件是x不為空、不是根節點、父節點的顏色是紅色(如果父節點不是紅色,則沒有連續的紅色節點,不再調整)
while (x != null && x != root && x.parent.color == RED) {
// x節點的父節點p(記作p)是其父節點pp(p的父節點,記作pp)的左孩子(pp的左孩子)
if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
// 獲取pp節點的右孩子r
Entry<K, V> y = rightOf(parentOf(parentOf(x)));
// pp右孩子的顏色是紅色(colorOf(Entry e)方法在e為空時返回BLACK),不需要進行旋轉操作(因為紅黑樹不是嚴格的平衡二叉樹)
if (colorOf(y) == RED) {
// 將父節點設定為黑色
setColor(parentOf(x), BLACK);
// y節點,即r設定成黑色
setColor(y, BLACK);
// pp節點設定成紅色
setColor(parentOf(parentOf(x)), RED);
// x“移動”到pp節點
x = parentOf(parentOf(x));
} else {
//父親的兄弟是黑色的,這時需要進行旋轉操作,根據是“內部”還是“外部”的情況決定是雙旋轉還是單旋轉
// x節點是父節點的右孩子(因為上面已近確認p是pp的左孩子,所以這是一個“內部,左-右”插入的情況,需要進行雙旋轉處理)
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的父節點(記作p)的父節點(記作pp)的左孩子
Entry<K, V> y = leftOf(parentOf(parentOf(x)));
// y節點是紅色的
if (colorOf(y) == RED) {
// x的父節點,即p節點,設定成黑色
setColor(parentOf(x), BLACK);
// y節點設定成黑色
setColor(y, BLACK);
// pp節點設定成紅色
setColor(parentOf(parentOf(x)), RED);
// x移動到pp節點
x = parentOf(parentOf(x));
} else {
// x是父節點的左孩子(因為上面已近確認p是pp的右孩子,所以這是一個“內部,右-左”插入的情況,需要進行雙旋轉處理),
if (x == leftOf(parentOf(x))) {
// x移動到父節點
x = parentOf(x);
// 右旋操作
rotateRight(x);
}
// x的父節點設定成黑色
setColor(parentOf(x), BLACK);
// x的父節點的父節點設定成紅色
setColor(parentOf(parentOf(x)), RED);
// 左旋操作
rotateLeft(parentOf(parentOf(x)));
}
}
}
// 根節點為黑色
root.color = BLACK;
}
而涉及到的左右旋轉的原始碼如下:
private void rotateLeft(Entry<K, V> p) {
if (p != null) {
Entry<K, V> r = p.right;
p.right = r.left;
if (r.left != null)
r.left.parent = p;
r.parent = p.parent;
if (p.parent == null)
root = r;
else if (p.parent.left == p)
p.parent.left = r;
else
p.parent.right = r;
r.left = p;
p.parent = r;
}
}
/** From CLR */
private void rotateRight(Entry<K, V> p) {
if (p != null) {
Entry<K, V> l = p.left;
p.left = l.right;
if (l.right != null)
l.right.parent = p;
l.parent = p.parent;
if (p.parent == null)
root = l;
else if (p.parent.right == p)
p.parent.right = l;
else
p.parent.left = l;
l.right = p;
p.parent = l;
}
}
左旋:
右旋:
獲取資料:
public V get(Object key) {
Entry<K, V> p = getEntry(key);
return (p == null ? null : p.value);
}
final Entry<K, V> getEntry(Object key) {
// 如果外部比較器,就採用外部比較器比對查詢元素
if (comparator != null)
return getEntryUsingComparator(key);
if (key == null)
throw new NullPointerException();
// 採用key的預設比較器
@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;
}
刪除資料:
public V remove(Object key) {
Entry<K, V> p = getEntry(key);
if (p == null)
return null;
V oldValue = p.value;
deleteEntry(p);
return oldValue;
}
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.
// 情況一:待刪除的節點有兩個孩子
if (p.left != null && p.right != null) {
Entry<K, V> s = successor(p);
p.key = s.key;
p.value = s.value;
p = s;
} // p has 2 children
// Start fixup at replacement node, if it exists.
Entry<K, V> replacement = (p.left != null ? p.left : p.right);
// 情況二:待刪除節點只有一個孩子
if (replacement != null) {
// Link replacement to parent
replacement.parent = p.parent;
if (p.parent == null)
root = replacement;
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.left = p.right = p.parent = null;
// Fix replacement
if (p.color == BLACK)
fixAfterDeletion(replacement);
} else if (p.parent == null) { // 情況三:根節點
root = null;
} else { // 情況四:無任何孩子節點
if (p.color == BLACK)
fixAfterDeletion(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;
}
}
}
deleteEntry(Entry e)方法中主要有兩個方法呼叫需要分析:successor(Entry<K,V> t)和fixAfterDeletion(Entry<K,V> x)。successor(Entry<K,V> t)返回指定節點的繼承者。分三種情況處理,第一。t節點是個空節點:返回null;第二,t有右孩子:找到t的右孩子中的最左子孫節點,如果右孩子沒有左孩子則返回右節點,否則返回找到的最左子孫節點;第三,t沒有右孩子:沿著向上(向跟節點方向)找到第一個自身是一個左孩子的節點或根節點,返回找到的節點。與新增節點之後的修復類似的是,TreeMap 刪除節點之後也需要進行類似的修復操作,通過這種修復來保證該排序二叉樹依然滿足紅黑樹特徵。大家可以參考插入節點之後的修復來分析刪除之後的修復。TreeMap 在刪除之後的修復操作由 fixAfterDeletion(Entry<K,V> x) 方法提供,以下就是這個兩個方法的原始碼:
static <K, V> TreeMap.Entry<K, V> successor(Entry<K, V> t) {
// 如果t本身是一個空節點,返回null
if (t == null)
return null;
// 如果t有右孩子,找到右孩子的最左子孫節點
else if (t.right != null) {
Entry<K, V> p = t.right;
while (p.left != null)
// 獲取p節點最左的子孫節點,如果存在的話
p = p.left;
// 返回找到的繼承節點
return p;
} else {
//t不為null且沒有右孩子
Entry<K, V> p = t.parent;
Entry<K, V> ch = t;
// 沿著右孩子向上查詢繼承者,直到根節點或找到節點ch是其父節點的左孩子的節點
while (p != null && ch == p.right) {
ch = p;
p = p.parent;
}
return p;
}
}
private void fixAfterDeletion(Entry<K, V> x) {
// 迴圈處理,條件為x不是root節點且是黑色的(因為紅色不會對紅黑樹的性質造成破壞,所以不需要調整)
while (x != root && colorOf(x) == BLACK) {
// x是一個左孩子
if (x == leftOf(parentOf(x))) {
// 獲取x的兄弟節點sib
Entry<K, V> sib = rightOf(parentOf(x));
if (colorOf(sib) == RED) {
setColor(sib, BLACK);
// 將父節點設定成紅色
setColor(parentOf(x), RED);
// 左旋父節點
rotateLeft(parentOf(x));
// sib移動到旋轉後x的父節點p的右孩子(參見左旋示意圖,獲取的節點是旋轉前p的右孩子r的左孩子rl)
sib = rightOf(parentOf(x));
}
// sib的兩個孩子的顏色都是黑色(null返回黑色)
if (colorOf(leftOf(sib)) == BLACK
&& colorOf(rightOf(sib)) == BLACK) {
setColor(sib, RED);
// x移動到x的父節點
x = parentOf(x);
} else {
// sib的左右孩子都是黑色的不成立
// sib的右孩子是黑色的
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);
}
其他的方法比較簡單,不在這裡一一列舉如需要了解,請查詢JDK1.8原始碼。
LinkedHashMap
類似於HashMap,但是迭代遍歷它時,它得“鍵值對”的順序是插入次序,或者是最近使用(LRU)次序。只是比HashMap慢一點,而在迭代訪問時反而更快,因為它使用連結串列維護內部次序。
LinkedHashMap的key跟vlaue都允許為空,並且key重複會覆蓋,value重複沒有影響,執行緒的不安全的。LinkedHashMap實現思想是多型。
底層原理:
定義:
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
屬性:
// 用於指向雙向連結串列的頭部
transient LinkedHashMap.Entry<K, V> head;
/**
* The tail (youngest) of the doubly linked list.
*/
//用於指向雙向連結串列的尾部
transient LinkedHashMap.Entry<K, V> tail;
/**
* The iteration ordering method for this linked hash map: <tt>true</tt> for
* access-order, <tt>false</tt> for insertion-order.
*
* @serial
*/
// 用來指定LinkedHashMap的迭代順序,
//true則表示按照基於訪問的順序來排列,意思就是最近使用的entry,放在連結串列的最末尾
//false則表示按照插入順序來
final boolean accessOrder;
構造方法:
/**
* Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance
* with the specified initial capacity and load factor.
*
* @param initialCapacity
* the initial capacity
* @param loadFactor
* the load factor
* @throws IllegalArgumentException
* if the initial capacity is negative or the load factor is
* nonpositive
*/
// 構造方法1,構造一個指定初始容量和負載因子的、按照插入順序的LinkedHashMap
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
/**
* Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance
* with the specified initial capacity and a default load factor (0.75).
*
* @param initialCapacity
* the initial capacity
* @throws IllegalArgumentException
* if the initial capacity is negative
*/
// 構造方法2,構造一個指定初始容量的LinkedHashMap,取得鍵值對的順序是插入順序
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
/**
* Constructs an empty insertion-ordered <tt>LinkedHashMap</tt> instance
* with the default initial capacity (16) and load factor (0.75).
*/
// 構造方法3,用預設的初始化容量和負載因子建立一個LinkedHashMap,取得鍵值對的順序是插入順序
public LinkedHashMap() {
super();
accessOrder = false;
}
/**
* Constructs an insertion-ordered <tt>LinkedHashMap</tt> instance with the
* same mappings as the specified map. The <tt>LinkedHashMap</tt> instance
* is created with a default load factor (0.75) and an initial capacity
* sufficient to hold the mappings in the specified map.
*
* @param m
* the map whose mappings are to be placed in this map
* @throws NullPointerException
* if the specified map is null
*/
// 構造方法4,通過傳入的map建立一個LinkedHashMap,容量為預設容量(16)和(map.zise()/DEFAULT_LOAD_FACTORY)+1的較大者,裝載因子為預設值
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super();
accessOrder = false;
putMapEntries(m, false);
}
/**
* Constructs an empty <tt>LinkedHashMap</tt> instance with the specified
* initial capacity, load factor and ordering mode.
*
* @param initialCapacity
* the initial capacity
* @param loadFactor
* the load factor
* @param accessOrder
* the ordering mode - <tt>true</tt> for access-order,
* <tt>false</tt> for insertion-order
* @throws IllegalArgumentException
* if the initial capacity is negative or the load factor is
* nonpositive
*/
// 構造方法5,根據指定容量、裝載因子和鍵值對保持順序建立一個LinkedHashMap
public LinkedHashMap(int initialCapacity, float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
從構造方法中可以看出,預設都採用插入順序來維持取出鍵值對的次序。所有構造方法都是通過呼叫父類的構造方法來建立物件的。
重要方法:
// get(Object key)方法通過HashMap的getEntry(Object key)方法獲取節點,並返回該節點的value值
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
//clear()方法先呼叫父類的方法clear()方法,之後將連結串列的header節點的before和after引用都指向header自身,
//即header節點就是一個雙向迴圈連結串列。這樣就無法訪問到原連結串列中剩餘的其他節點,他們都將被GC回收。
public void clear() {
super.clear();
head = tail = null;
}
// 重寫父類的containsValue(Object value)方法,直接通過header遍歷連結串列判斷是否有值和value相等,而不用查詢table陣列。
public boolean containsValue(Object value) {
for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after) {
V v = e.value;
if (v == value || (value != null && value.equals(v)))
return true;
}
return false;
}
而put方法直接使用了HashMap的put方法,所以不再列舉。