想看我更多文章:【張旭童的博客】blog.csdn.net/zxt0601
想來gayhub和我gaygayup:【mcxtzhang的Github主頁】github.com/mcxtzhangnode
在上文中,咱們已經聊過了HashMap
,本篇是基於上文的基礎之上。因此若是沒看過上文,請先閱讀面試必備:HashMap源碼解析(JDK8)
本文將從幾個經常使用方法下手,來閱讀LinkedHashMap
的源碼。
按照從構造方法->經常使用API(增、刪、改、查)的順序來閱讀源碼,並會講解閱讀方法中涉及的一些變量的意義。瞭解LinkedHashMap
的特色、適用場景。git
若是本文中有不正確的結論、說法,請你們提出和我討論,共同進步,謝謝。github
歸納的說,LinkedHashMap
是一個關聯數組、哈希表,它是線程不安全的,容許key爲null,value爲null。
它繼承自HashMap
,實現了Map<K,V>
接口。其內部還維護了一個雙向鏈表,在每次插入數據,或者訪問、修改數據時,會增長節點、或調整鏈表的節點順序。以決定迭代時輸出的順序。面試
默認狀況,遍歷時的順序是按照插入節點的順序。這也是其與HashMap
最大的區別。
也能夠在構造時傳入accessOrder
參數,使得其遍歷順序按照訪問的順序輸出。數組
因繼承自HashMap
,因此HashMap
上文分析的特色,除了輸出無序,其餘LinkedHashMap
都有,好比擴容的策略,哈希桶長度必定是2的N次方等等。LinkedHashMap
在實現時,就是重寫override了幾個方法。以知足其輸出序列有序的需求。安全
根據這段實例代碼,先從現象看一下LinkedHashMap
的特徵:
在每次插入數據,或者訪問、修改數據時,會增長節點、或調整鏈表的節點順序。以決定迭代時輸出的順序。bash
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());
}複製代碼
輸出:ide
1=a
2=b
3=c
4=d
如下是accessOrder=true的狀況:
1=a
2=b
4=d
3=e
null=null
5=null複製代碼
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
,分別指向內部雙向鏈表的表頭、表尾。post
//雙向鏈表的頭結點
transient LinkedHashMap.Entry<K,V> head;
//雙向鏈表的尾節點
transient LinkedHashMap.Entry<K,V> tail;複製代碼
//默認是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
參數。用於控制迭代時的節點順序。
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
裏能夠忽略它們。
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;
}複製代碼
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
,由於迭代的順序已經改變。
它重寫了該方法,相比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;
}複製代碼
重寫了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是否相等呢?