前面幾篇博客Java容器之Hashtable源碼分析、Java容器之HashMap源碼分析分別分析了HashMap
、Hashtable
的源碼,此篇博客我們分析一下LinkedHashMap
容器,看看它又有什麼花樣。
註明:以下源碼分析都是基於jdk 1.8.0_221
LinkedHashMap源碼分析目錄
一、LinkedHashMap
容器概述(一圖以蔽之)
LinkedHashMap
類的申明如下:
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
可以看出LinkedHashMap
是HashMap
子類(建議先看看HashMap
再來看捏 → Java容器之HashMap源碼分析),大膽推測一下,LinkedHashMap
底層用HashMap
存儲節點,只是展示給你的是一個Linked
狀態。
1、LinkedHashMap
容器的自我定位
看過博主前面HashMap
、Hashtable
源碼分析或者瞭解一點Map
結構的小夥伴,肯定知道HashMap
的定位是單線程處理(讀、寫高效),Hashtable
的定位是支持併發讀寫(引入鎖機制,降低效率),那麼這個LinkedHashMap
的定位又是什麼呢?先來看個例子:
public class Main {
public static void main(String[] args) {
// 新建一個linkedHashMap
LinkedHashMap<String, String> linkedHashMap = new LinkedHashMap<>();
// 依次插入三個key-value
linkedHashMap.put("老王", "我是誰");
linkedHashMap.put("老趙", "我在哪");
linkedHashMap.put("老李", "我要幹嘛");
// 遍歷輸出linkedHashMap中的key-value
for (Map.Entry<String, String> entry : linkedHashMap.entrySet()) {
System.out.println(entry.getKey() + "\t" + entry.getValue());
}
}
}
可以看出LinkedHashMap
能按照插入的順序給你輸出,即一種Linked
的狀態,其實這就是它的最主要特點。
2、LinkedHashMap
容器數據結構圖
LinkedHashMap
容器比較簡單,所有的key-value
節點都是存在HashMap
容器中(前面說過LinkedHashMap
繼承HashMap
),只是加上before
、after
指針。一句話概括就是 LinkedHashMap
= HashMap
+ 雙鏈表。
註明:在JDK1.8中,HashMap
的hash桶
實現引入了紅黑樹(參考Java容器之HashMap源碼分析),但是我們此處主要是討論LinkedHashMap
的Linked
屬性,爲了方便畫圖,所以結構示意圖沒有畫出來。
二、LinkedHashMap
類的屬性與內部類
1、Entry
內部類
在HashMap
存儲key-value
的類爲Node
(屬性主要包括hash
、key
、value
),在LinkedHashMap
類用於要記錄Linked
狀態,所以增加一個繼承Node
的內部類Entry
。Entry
類只是增加before
、after
指針而已,在LinkedHashMap
用Entry
對象存key-value
。
/**
* Entry繼承HashMap.Node
*/
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);
}
}
2、LinkedHashMap
新增屬性
LinkedHashMap
類中新增三個主要的屬性,head
、tail
分別指向外部展示的list
頭部、尾部,accessOrder
屬性的作用是list中節點順序是否隨get
方法調用而改變。
/**
* Linked list 頭部
*/
transient LinkedHashMap.Entry<K,V> head;
/**
* Linked list 尾部
*/
transient LinkedHashMap.Entry<K,V> tail;
/**
* = true,每次調用get方法都會將查找到的key-value放入尾部
*/
final boolean accessOrder;
備註
:這裏的accessOrder
有人翻爲訪問順序
,其實根本不用翻譯這個詞。因爲它容器與插入順序
搞混,從而搞亂LinkedHashMap
有序到底什麼有序。你只要記住LinkedHashMap
是key-value
插入順序有序,accessOrder
變量的作用是調用get方法是否會將查找到的節點放入尾部即可!
三、LinkedHashMap
類構造器
LinkedHashMap
類一共有5個構造器,涉及initialCapacity
、loadFactor
、accessOrder
三個參數的初始化。
/**
* @param initialCapacity 初始容量(hashMap.table數組長度)
* @param loadFactor 負載因子(hashMap中的參數,
* initialCapacity * loadFactor就是HashMap容器可存放的key-value閾值,這裏不再重複介紹了)
*/
public LinkedHashMap(int initialCapacity, float loadFactor) {
// super關鍵字,調用父類HashMap的響應構造器
super(initialCapacity, loadFactor);
// 默認維護的list不隨get方法調用改變
accessOrder = false;
}
/**
* @param initialCapacity 初始容量(hashMap.table數組長度)
*/
public LinkedHashMap(int initialCapacity) {
// super關鍵字,調用父類HashMap的響應構造器,負載因子默認是0.75
super(initialCapacity);
// 默認維護的list不隨get方法調用改變
accessOrder = false;
}
/**
* HashMap.table長度默認16,HashMap默認負載因子爲0.75
*/
public LinkedHashMap() {
// super關鍵字,調用父類HashMap的響應構造器
super();
// 默認維護的list不隨get方法調用改變
accessOrder = false;
}
/**
* 複製構造方法
*/
public LinkedHashMap(Map<? extends K, ? extends V> m) {
// 先初始化父類HashMap
super();
accessOrder = false;
// 然後將容器m中的所有元素複製到當前容器
putMapEntries(m, false);
}
/**
* @param initialCapacity 初始容量(hashMap.table數組長度)
* @param loadFactor 負載因子(hashMap中的參數,initialCapacity * loadFactor就是HashMap容器可存放的key-value閾值
* @param accessOrder 維護的list是否隨get方法調用而修改
*/
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
四、Linked list
維護原理分析
前面多次提及到LinkedHashMap
容器中維護了一個鏈表使得展示給外界是按key-value插入順序排列的,下面我們來看看它到底是如何維護這個虛擬的Linked list
。
1、相關方法
與Linked list
維護相關的方法有三個,分別如下:
①、afterNodeRemoval
方法
afterNodeRemoval
,顧名思義,當刪除了某個節點後需要做的操作。
/**
* afterNodeRemoval此方法是將節點e從list中刪除,調用hashMap.remove方法已經將它從hashMap中刪除(注意看前面的LinkedHashMap數據結構示意圖,有兩套指針,
* 一套是HashMap中的hash桶內元素連接的黑色指針,只要把節點上下關係移除即可
* 另外一套是有顏色的指針,它是維護list的指針,從hash桶中刪除後,還有從list中刪除)
*/
void afterNodeRemoval(Node<K,V> e) {
// 標記節點e、e.before、e.after
LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
// 第一步:p的前後置空,選擇e已經從鏈表中移除
p.before = p.after = null;
// 第二步:還要將e前驅.after = e的後繼
if (b == null)
// b是操作前e.before==null,說明e是頭結點,head更新爲e.after
head = a;
else
// 否則更新e的前驅.after == e的後繼節點
b.after = a;
// 第三步:將e的後繼.before = e的前驅
if (a == null)
// 如果沒刪除前e.after後繼 == null,說明e處於list的尾部,將tail指向e的前驅節點
tail = b;
else
// 否則更新e的後繼.before = e的前驅
a.before = b;
}
②、afterNodeInsertion
方法
afterNodeInsertion
,顧名思義,當插入了某個節點後需要做的操作。不過這個方法有些奇怪,與我們的預期不符,但是LinkedHashMap
插入節點後做的啥操作呢?先賣個關子~,繼續看吧 ~
/**
* 按照之前的想法,插入一個節點後應該是將插入的節點放入tailde後面,但是這裏並不是這樣幾的
*/
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
// LinkedHashMap中的removeEldestEntry方法永遠返回false(方法體見下文),也就說這個本方法不會執行任何操作。。。
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
/**
* removeEldestEntry方法永遠返回false
*/
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
③、afterNodeAccess
方法
afterNodeAccess
,從名字來看,這個方法應該是訪問了某個key-value
後的操作(還記得我們之前特意提到的屬性accessOrder
嗎?不記得往前翻翻)。當accessOrder == true
時,每當我們調用get
方法,都會將查找到的key-value
移動到鏈表的尾端。
/**
* 此方法的作用是將剛剛訪問的節點e放到鏈表的尾端
*/
void afterNodeAccess(Node<K,V> e) {
LinkedHashMap.Entry<K,V> last;
// accessOrder = true 時 訪問節點後才需要置於尾端
// 如果e本身就在尾端,那就不需要操作
if (accessOrder && (last = tail) != e) {
// 記錄節點e、e的前驅、e的後繼
LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
// 第一步:現將p.after置空
p.after = null;
// 第二步:將e的前驅.after 連接上e的後繼
if (b == null)
// b記錄e的前驅,前驅爲null,則e在表頭,head置爲e的後繼
head = a;
else
// 否則 e的前驅.after = e的後繼
b.after = a;
// 第三步:將e的後繼.before 連接上e的前驅
if (a != null)
// e的後繼 != null,將e後繼.before = e的前驅
a.before = b;
else
// 否則e的後繼 == null,即在e表尾(這裏有點多餘,前面已經判斷在表尾不操作。。。)
last = b;
// 第四步:將節點e接入到鏈表的尾端
if (last == null)
// last == null,鏈表爲空,head = p
head = p;
else {
// p.before 指向last(鏈表尾端),尾端.after = p
p.before = last;
last.after = p;
}
// 第四步:更新鏈表新尾端tail
tail = p;
// 鏈表結構性調整,修改次數自增
++modCount;
}
}
2、維護鏈表方法調用
前面介紹了維護鏈表的方法,但是還沒介紹哪裏調用了捏,別急,這就來說了~
①、get
方法調用的調整
LinkedHashMap
重寫了get
方法。
public V get(Object key) {
Node<K,V> e;
// 調用HashMap.getNode方法,前面說過LinkedHashMap中含有HashMap存放數據,當然得調用這個方法查找,這不是關鍵,沒找到就返null
if ((e = getNode(hash(key), key)) == null)
return null;
// accessOrder = true,訪問之後要將訪問節點放到鏈表尾端,afterNodeAccess方法上面剛講的,不用再說了吧~
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
②、getOrDefault
方法調用的調整
LinkedHashMap
提供一個getOrDefault
方法,沒找到就返回提供的默認值。
/**
* {@inheritDoc}
*/
public V getOrDefault(Object key, V defaultValue) {
Node<K,V> e;
// 調用HashMap.getNode查找,沒找到就返回默認值
if ((e = getNode(hash(key), key)) == null)
return defaultValue;
// accessOrder = true,訪問之後要將訪問節點放到鏈表尾端,afterNodeAccess方法上面剛講的,不用再說了吧~
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
③、put
方法調用的調整
在LinkedHashMap
中並沒有重寫put
相關的方法,直接調用HashMap父類put
相關的方法就行。那麼問題來了,父類HashMap中的put方法沒有定義維護鏈表相關的方法啊,哪如何維護鏈表呢?
其實在HashMap
類中定了前面介紹的三個方法的空實現!!!
那麼HashMap
put相關的方法有調用這三個方法嗎?請看下面:(在Java容器之HashMap源碼分析詳細分析了這個方法,這裏不再重複,直接看關鍵調用即可。)
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
/**
*
* @param hash key的hash值(調用hash()方法)
* @param key 插入鍵值對key
* @param value 插入鍵值對value
* @param onlyIfAbsent 設爲true時,表示如果容器已經存在這個key就不進行修改
* @param evict 爲 false時,表示容器正處於創建(其它map傳入初始化)
* @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;
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;
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);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
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,前面說afterNodeInsertion方法什麼也不幹,
// 其實插入成功之後調用afterNodeAccess放入尾端,效果是一樣的
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
這裏可能有部分懂一點面向對象編程OOP
的朋友可能會有疑問,子類重寫的方法(前面介紹的三個維護鏈表的方法),在父類中調用會調用到重寫的方法還是本類的方法(三個空實現)?
咳咳,這裏我裝下筆,面向對象編程OOP
三大特徵,封裝
、繼承
、多態
。
首先我們存key-value
是LinkedHashMap
對象吧,那麼調用父類hashMap.get
方法時,此時還是不是LinkedHashMap
對象?顯然是吧,那麼在hashMap.get
中調用afterNodeAccess
、afterNodeInsertion
方法同樣是LinkedHashMap
對象吧,那當然是調用LinkedHashMap.afterNodeAccess
、LinkedHashMap.afterNodeInsertion
方法了(這裏就是一個多態場景!)如果你不懂話,那就算了,記住LinkedHashMap
對象調用hashMap.get
時,會調用子類LinkedHashMap
中的方法即可。
④、remove
方法調用的調整
在LinkedHashMap
中並沒有重寫remove
相關的方法,直接調用HashMap
父類remove
相關的方法就行。那麼HashMap
父類remove
相關的方法也有調用afterNodeRemoval
方法嘛?答案是確定的,附上HashMap
父類remove
相關的方法源碼(在Java容器之HashMap源碼分析詳細分析了這個方法,這裏不再重複,直接看關鍵調用即可。)
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
/**
* Implements Map.remove and related methods.
*
* @param hash key對應的hash(調用hash()方法即可計算得到)
* @param key 待刪除的key-value對應的key
* @param value key-value對應的value,只有當matchValue == true時,此參數纔有意義
* @param matchValue 如果設爲true,刪除的時候還需要匹配value才能刪
* @param movable 設爲false,表示刪除成功了不移動其它節點(一般設爲true,即刪除節點後需要進行調整)
* @return 刪除成功則返回key對應的value,否則返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;
} while ((e = e.next) != null);
}
}
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 方法進行了鏈表的維護
afterNodeRemoval(node);
return node;
}
}
return null;
}
上面介紹了get
、remove
、put
三種方法對應的鏈表維護調整,可能會有部分小夥伴覺得擴容也需要維護鏈表。但事實上擴容是不需要調整鏈表的,因爲擴容的本質是將key-value移動到其它hash桶
(table
數組其它鏈表下),並沒有改變放入容器的順序,不信你可以想想把LinkedHashMap
容器數據結構示意圖中的一個節點移動到其它hash桶下
,看看他會不會改變list
順序。
五、總結
LinkedHashMap
是HashMap
的子類,並且key-value
都是放在HashMap.table
中存儲,只是把所有key-value
按照放入容器的順序用雙鏈表串了起來。那麼HashMap
的特徵LinkedHashMap
都有,比如不支持併發讀寫、允許放入key
或value
爲空的key-value
。LinkedHashMap
還有一個特性就是可以按照放入容器的順序取出來。
總的來說,一句話 LinkedHashMap
= HashMap
+ list
。