想看我更多文章:【張旭童的部落格】blog.csdn.net/zxt0601
想來gayhub和我gaygayup:【mcxtzhang的Github主頁】github.com/mcxtzhang
1 概述
在上文中,我們已經聊過了HashMap
,本篇是基於上文的基礎之上。所以如果沒看過上文,請先閱讀面試必備:HashMap原始碼解析(JDK8)
本文將從幾個常用方法下手,來閱讀LinkedHashMap
的原始碼。
按照從構造方法->常用API(增、刪、改、查)的順序來閱讀原始碼,並會講解閱讀方法中涉及的一些變數的意義。瞭解LinkedHashMap
的特點、適用場景。
如果本文中有不正確的結論、說法,請大家提出和我討論,共同進步,謝謝。
2 概要
概括的說,LinkedHashMap
是一個關聯陣列、雜湊表,它是執行緒不安全的,允許key為null,value為null。
它繼承自HashMap
,實現了Map<K,V>
介面。其內部還維護了一個雙向連結串列,在每次插入資料,或者訪問、修改資料時,會增加節點、或調整連結串列的節點順序。以決定迭代時輸出的順序。
預設情況,遍歷時的順序是按照插入節點的順序。這也是其與HashMap
最大的區別。
也可以在構造時傳入accessOrder
引數,使得其遍歷順序按照訪問的順序輸出。
因繼承自HashMap
,所以HashMap
上文分析的特點,除了輸出無序,其他LinkedHashMap
都有,比如擴容的策略,雜湊桶長度一定是2的N次方等等。LinkedHashMap
在實現時,就是重寫override了幾個方法。以滿足其輸出序列有序的需求。
示例程式碼:
根據這段例項程式碼,先從現象看一下LinkedHashMap
的特徵:
在每次插入資料,或者訪問、修改資料時,會增加節點、或調整連結串列的節點順序。以決定迭代時輸出的順序。
Map<String, String> map = new LinkedHashMap<>();
map.put("1", "a");
map.put("2", "b");
map.put("3", "c");
map.put("4", "d");
Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
System.out.println("以下是accessOrder=true的情況:");
map = new LinkedHashMap<String, String>(10, 0.75f, true);
map.put("1", "a");
map.put("2", "b");
map.put("3", "c");
map.put("4", "d");
map.get("2");//2移動到了內部的連結串列末尾
map.get("4");//4調整至末尾
map.put("3", "e");//3調整至末尾
map.put(null, null);//插入兩個新的節點 null
map.put("5", null);//5
iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}複製程式碼
輸出:
1=a
2=b
3=c
4=d
以下是accessOrder=true的情況:
1=a
2=b
4=d
3=e
null=null
5=null複製程式碼
3 節點
LinkedHashMap
的節點Entry<K,V>
繼承自HashMap.Node<K,V>
,在其基礎上擴充套件了一下。改成了一個雙向連結串列。
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}複製程式碼
同時類裡有兩個成員變數head tail
,分別指向內部雙向連結串列的表頭、表尾。
//雙向連結串列的頭結點
transient LinkedHashMap.Entry<K,V> head;
//雙向連結串列的尾節點
transient LinkedHashMap.Entry<K,V> tail;複製程式碼
4 建構函式
//預設是false,則迭代時輸出的順序是插入節點的順序。若為true,則輸出的順序是按照訪問節點的順序。
//為true時,可以在這基礎之上構建一個LruCach
final boolean accessOrder;
public LinkedHashMap() {
super();
accessOrder = false;
}
//指定初始化時的容量,
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
//指定初始化時的容量,和擴容的載入因子
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
//指定初始化時的容量,和擴容的載入因子,以及迭代輸出節點的順序
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
//利用另一個Map 來構建,
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super();
accessOrder = false;
//該方法上文分析過,批量插入一個map中的所有資料到 本集合中。
putMapEntries(m, false);
}複製程式碼
小結:
建構函式和HashMap
相比,就是增加了一個accessOrder
引數。用於控制迭代時的節點順序。
5 增
LinkedHashMap
並沒有重寫任何put方法。但是其重寫了構建新節點的newNode()
方法.newNode()
會在HashMap
的putVal()
方法裡被呼叫,putVal()
方法會在批量插入資料putMapEntries(Map<? extends K, ? extends V> m, boolean evict)
或者插入單個資料public V put(K key, V value)
時被呼叫。
LinkedHashMap
重寫了newNode()
,在每次構建新節點時,通過linkNodeLast(p);
將新節點連結在內部雙向連結串列的尾部。
//在構建新節點時,構建的是`LinkedHashMap.Entry` 不再是`Node`.
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
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;
}
}複製程式碼
以及HashMap
專門預留給LinkedHashMap
的afterNodeAccess() afterNodeInsertion() afterNodeRemoval()
方法。
// Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }複製程式碼
//回撥函式,新節點插入之後回撥 , 根據evict 和 判斷是否需要刪除最老插入的節點。如果實現LruCache會用到這個方法。
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
//LinkedHashMap 預設返回false 則不刪除節點
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
//LinkedHashMap 預設返回false 則不刪除節點。 返回true 代表要刪除最早的節點。通常構建一個LruCache會在達到Cache的上限是返回true
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}複製程式碼
void afterNodeInsertion(boolean evict)
以及boolean removeEldestEntry(Map.Entry<K,V> eldest)
是構建LruCache需要的回撥,在LinkedHashMap
裡可以忽略它們。
6 刪
LinkedHashMap
也沒有重寫remove()
方法,因為它的刪除邏輯和HashMap
並無區別。
但它重寫了afterNodeRemoval()
這個回撥方法。該方法會在Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable)
方法中回撥,removeNode()
會在所有涉及到刪除節點的方法中被呼叫,上文分析過,是刪除節點操作的真正執行者。
//在刪除節點e時,同步將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 的前置後置節點都置空
p.before = p.after = null;
//如果前置節點是null,則現在的頭結點應該是後置節點a
if (b == null)
head = a;
else//否則將前置節點b的後置節點指向a
b.after = a;
//同理如果後置節點時null ,則尾節點應是b
if (a == null)
tail = b;
else//否則更新後置節點a的前置節點為b
a.before = b;
}複製程式碼
7 查
LinkedHashMap
重寫了get()和getOrDefault()
方法:
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;
}
public V getOrDefault(Object key, V defaultValue) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return defaultValue;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}複製程式碼
對比HashMap
中的實現,LinkedHashMap
只是增加了在成員變數(建構函式時賦值)accessOrder
為true的情況下,要去回撥void afterNodeAccess(Node<K,V> e)
函式。
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}複製程式碼
在afterNodeAccess()
函式中,會將當前被訪問到的節點e,移動至內部的雙向連結串列的尾部。
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;//原尾節點
//如果accessOrder 是true ,且原尾節點不等於e
if (accessOrder && (last = tail) != e) {
//節點e強轉成雙向連結串列節點p
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
//p現在是尾節點, 後置節點一定是null
p.after = null;
//如果p的前置節點是null,則p以前是頭結點,所以更新現在的頭結點是p的後置節點a
if (b == null)
head = a;
else//否則更新p的前直接點b的後置節點為 a
b.after = a;
//如果p的後置節點不是null,則更新後置節點a的前置節點為b
if (a != null)
a.before = b;
else//如果原本p的後置節點是null,則p就是尾節點。 此時 更新last的引用為 p的前置節點b
last = b;
if (last == null) //原本尾節點是null 則,連結串列中就一個節點
head = p;
else {//否則 更新 當前節點p的前置節點為 原尾節點last, last的後置節點是p
p.before = last;
last.after = p;
}
//尾節點的引用賦值成p
tail = p;
//修改modCount。
++modCount;
}
}複製程式碼
值得注意的是,afterNodeAccess()
函式中,會修改modCount
,因此當你正在accessOrder=true
的模式下,迭代LinkedHashMap
時,如果同時查詢訪問資料,也會導致fail-fast
,因為迭代的順序已經改變。
7.2 containsValue
它重寫了該方法,相比HashMap
的實現,更為高效。
public boolean containsValue(Object value) {
//遍歷一遍連結串列,去比較有沒有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;
}複製程式碼
對比HashMap
,是用兩個for迴圈遍歷,相對低效。
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;
}複製程式碼
8 遍歷
重寫了entrySet()
如下:
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
//返回LinkedEntrySet
return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
}
final class LinkedEntrySet extends AbstractSet<Map.Entry<K,V>> {
public final Iterator<Map.Entry<K,V>> iterator() {
return new LinkedEntryIterator();
}
}複製程式碼
最終的EntryIterator:
final class LinkedEntryIterator extends LinkedHashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode(); }
}
abstract class LinkedHashIterator {
//下一個節點
LinkedHashMap.Entry<K,V> next;
//當前節點
LinkedHashMap.Entry<K,V> current;
int expectedModCount;
LinkedHashIterator() {
//初始化時,next 為 LinkedHashMap內部維護的雙向連結串列的扁頭
next = head;
//記錄當前modCount,以滿足fail-fast
expectedModCount = modCount;
//當前節點為null
current = null;
}
//判斷是否還有next
public final boolean hasNext() {
//就是判斷next是否為null,預設next是head 表頭
return next != null;
}
//nextNode() 就是迭代器裡的next()方法 。
//該方法的實現可以看出,迭代LinkedHashMap,就是從內部維護的雙連結串列的表頭開始迴圈輸出。
final LinkedHashMap.Entry<K,V> nextNode() {
//記錄要返回的e。
LinkedHashMap.Entry<K,V> e = next;
//判斷fail-fast
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
//如果要返回的節點是null,異常
if (e == null)
throw new NoSuchElementException();
//更新當前節點為e
current = e;
//更新下一個節點是e的後置節點
next = e.after;
//返回e
return e;
}
//刪除方法 最終還是呼叫了HashMap的removeNode方法
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;
}
}複製程式碼
值得注意的就是:nextNode()
就是迭代器裡的next()
方法 。
該方法的實現可以看出,迭代LinkedHashMap
,就是從內部維護的雙連結串列的表頭開始迴圈輸出。
而雙連結串列節點的順序在LinkedHashMap
的增、刪、改、查時都會更新。以滿足按照插入順序輸出,還是訪問順序輸出。
總結
LinkedHashMap
相對於HashMap
的原始碼比,是很簡單的。因為大樹底下好乘涼。它繼承了HashMap
,僅重寫了幾個方法,以改變它迭代遍歷時的順序。這也是其與HashMap
相比最大的不同。
在每次插入資料,或者訪問、修改資料時,會增加節點、或調整連結串列的節點順序。以決定迭代時輸出的順序。
accessOrder
,預設是false,則迭代時輸出的順序是插入節點的順序。若為true,則輸出的順序是按照訪問節點的順序。為true時,可以在這基礎之上構建一個LruCache
.LinkedHashMap
並沒有重寫任何put方法。但是其重寫了構建新節點的newNode()
方法.在每次構建新節點時,將新節點連結在內部雙向連結串列的尾部accessOrder=true
的模式下,在afterNodeAccess()
函式中,會將當前被訪問到的節點e,移動至內部的雙向連結串列的尾部。值得注意的是,afterNodeAccess()
函式中,會修改modCount
,因此當你正在accessOrder=true
的模式下,迭代LinkedHashMap
時,如果同時查詢訪問資料,也會導致fail-fast
,因為迭代的順序已經改變。nextNode()
就是迭代器裡的next()
方法 。
該方法的實現可以看出,迭代LinkedHashMap
,就是從內部維護的雙連結串列的表頭開始迴圈輸出。
而雙連結串列節點的順序在LinkedHashMap
的增、刪、改、查時都會更新。以滿足按照插入順序輸出,還是訪問順序輸出。- 它與
HashMap
比,還有一個小小的優化,重寫了containsValue()
方法,直接遍歷內部連結串列去比對value值是否相等。
那麼,還有最後一個小問題?為什麼它不重寫containsKey()
方法,也去迴圈比對內部連結串列的key是否相等呢?