一、前言
在上一篇隨筆中,我們分析了HashMap的源碼,里面涉及到了3個鈎子函數,用來預設給子類——LinkedHashMap的調用,所以趁熱打鐵,今天我們來一起看一下它的源碼吧。
二、LinkedHashMap的結構與繼承關系
### 2.1 LinkedHashMap的數據結構
可以從上圖中看到,LinkedHashMap數據結構相比較於HashMap來說,添加了雙向指針,分別指向前一個節點——before和后一個節點——after,從而將所有的節點已鏈表的形式串聯一起來,從名字上來看LinkedHashMap與HashMap有一定的聯系,實際上也確實是這樣,LinkedHashMap繼承了HashMap,重寫了HashMap的一部分方法,從而加入了鏈表的實現。讓我們來看一下它們的繼承關系。
2.2 LinkedHashMap的繼承關系
2.2.1 Entry的繼承關系

Entry作為基本的節點,可以看到LinkedHashMap的Entry繼承自HashMap的Node,在其基礎上加上了before和after兩個指針,而TreeNode作為HashMap和LinkedHashMap的樹節點,繼承自LinkedHahsMap的Entry,並且加上了樹節點的相關指針,另外提一點:before和parent的兩個概念是不一樣的,before是相對於鏈表來的,parent是相對於樹操作來的,所以要分兩個。
2.2.2 Iterator的繼承關系

LinkedHashMap的迭代器為遍歷節點提供了自己的實現——LinkedHashIterator,對於Key、Value、Entry的3個迭代器,都繼承自它。而且內部采用的遍歷方式就是在前面提到的Entry里加的新的指向下一個節點的指針after,后面我們將具體看它的代碼實現。
三、LinkedHashMap源碼解析
本節我們將結合HashMap的部分源碼一起分析一下LinkedHashMap。
3.1 LinkedHashMap的繼承關系
LinekdHashMap的繼承關系前面已經說到了,不過按照習慣還是先放上去,湊一下字數 :)
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
這里沒什么好說的,繼承自HashMap,實現了Map接口
3.2 LinkedHashMap的成員變量
private static final long serialVersionUID = 3801124242820219131L;
// 用於指向雙向鏈表的頭部
transient LinkedHashMap.Entry<K,V> head;
//用於指向雙向鏈表的尾部
transient LinkedHashMap.Entry<K,V> tail;
/**
* 用來指定LinkedHashMap的迭代順序,
* true則表示按照基於訪問的順序來排列,意思就是最近使用的entry,放在鏈表的最末尾
* false則表示按照插入順序來
*/
final boolean accessOrder;
注意:accessOrder的final關鍵字,說明我們要在構造方法里給它初始化。
至於Entry的數據結構在第二節的圖里面有了,這里就不重復了哈。
3.3 LinkedHashMap的構造方法
跟HashMap類似的構造方法這里就不一一贅述了,里面唯一的區別就是添加了前面提到的accessOrder,默認賦值為false——按照插入順序來排列,這里主要說明一下不同的構造方法。
//多了一個 accessOrder的參數,用來指定按照LRU排列方式還是順序插入的排序方式
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
3.4 LinkedHashMap的get()方法
可能會有園友好奇,LinkedHashMap是怎么加上雙向鏈表的呢,我們先來看一下get()方法
public V get(Object key) {
Node<K,V> e;
//調用HashMap的getNode的方法,詳見上一篇HashMap源碼解析
if ((e = getNode(hash(key), key)) == null)
return null;
//在取值后對參數accessOrder進行判斷,如果為true,執行afterNodeAccess
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
從上面的代碼可以看到,LinkedHashMap的get方法,調用HashMap的getNode方法后,對accessOrder的值進行了判斷,我們之前提到:
accessOrder為true則表示按照基於訪問的順序來排列,意思就是最近使用的entry,放在鏈表的最末尾
由此可見,afterNodeAccess(e)就是基於訪問的順序排列的關鍵,讓我們來看一下它的代碼:
//此函數執行的效果就是將最近使用的Node,放在鏈表的最末尾
void afterNodeAccess(Node<K,V> e) {
LinkedHashMap.Entry<K,V> last;
//僅當按照LRU原則且e不在最末尾,才執行修改鏈表,將e移到鏈表最末尾的操作
if (accessOrder && (last = tail) != e) {
//將e賦值臨時節點p, b是e的前一個節點, a是e的后一個節點
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
//設置p的后一個節點為null,因為執行后p在鏈表末尾,after肯定為null
p.after = null;
//p前一個節點不存在,情況一
if (b == null) // ①
head = a;
else
b.after = a;
if (a != null)
a.before = b;
//p的后一個節點不存在,情況二
else // ②
last = b;
//情況三
if (last == null) // ③
head = p;
//正常情況,將p設置為尾節點的准備工作,p的前一個節點為原先的last,last的after為p
else {
p.before = last;
last.after = p;
}
//將p設置為將p設置為尾節點
tail = p;
// 修改計數器+1
++modCount;
}
}
標注的情況如下圖所示(特別說明一下,這里是顯示鏈表的修改后指針的情況,實際上在桶里面的位置是不變的,只是前后的指針指向的對象變了):

下面來簡單說明一下:
-
正常情況下:查詢的p在鏈表中間,那么將p設置到末尾后,它原先的前節點b和后節點a就變成了前后節點。
-
情況一:p為頭部,前一個節點b不存在,那么考慮到p要放到最后面,則設置p的后一個節點a為head
-
情況二:p為尾部,后一個節點a不存在,那么考慮到統一操作,設置last為b
-
情況三:p為鏈表里的第一個節點,head=p
3.5 LinkedHashMap的put()方法
接下來,讓我們來看一下LinkedHashMap是怎么插入Entry的:LinkedHashMap的put方法調用的還是HashMap里的put,不同的是重寫了里面的部分方法,一起來看一下:
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
...
tab[i] = newNode(hash, key, value, null);
...
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
...
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
...
afterNodeAccess(e);
...
afterNodeInsertion(evict);
return null;
}
由於在上一章分析過了put方法,這里筆者就省略了部分代碼,LinkedHashMap將其中newNode方法以及之前設置下的鈎子方法afterNodeAccess和afterNodeInsertion進行了重寫,從而實現了加入鏈表的目的。一起來看一下:
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
//秘密就在於 new的是自己的Entry類,然后調用了linkedNodeLast
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) {
//將tail給臨時變量last
LinkedHashMap.Entry<K,V> last = tail;
//把new的Entry給tail
tail = p;
//若沒有last,說明p是第一個節點,head=p
if (last == null)
head = p;
//否則就做准備工作,你懂的 ( ̄▽ ̄)"
else {
p.before = last;
last.after = p;
}
}
//這里筆者也把TreeNode的重寫也加了進來,因為putTreeVal里有調用了這個
TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
linkNodeLast(p);
return p;
}
//插入后把最老的Entry刪除,不過removeEldestEntry總是返回false,所以不會刪除,估計又是一個鈎子方法給子類用的
void afterNodeInsertion(boolean evict) {
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
總結:設計者靈活的運用了Override,以及設置的鈎子方法,實現了雙向鏈表。
3.6 LinkedHashMap的remove()
上一章我們提到過remove里面設計者也設置了一個鈎子方法:
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
...
//node即是要刪除的節點
afterNodeRemoval(node);
...
}
一起來看一下這個方法干了什么:
void afterNodeRemoval(Node<K,V> e) {
//與afterNodeAccess一樣,記錄e的前后節點b,a
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
//p已刪除,前后指針都設置為null,便於GC回收
p.before = p.after = null;
//與afterNodeAccess一樣類似,一頓判斷,然后b,a互為前后節點
if (b == null)
head = a;
else
b.after = a;
if (a == null)
tail = b;
else
a.before = b;
}
remove里的相對簡單,順帶着簡單提一提。
3.7 LinkedHashMap的迭代器
這一節,讓我們來看一下LinkedHashMap的最基礎的迭代器——LinkedHashIterator
abstract class LinkedHashIterator {
//記錄下一個Entry
LinkedHashMap.Entry<K,V> next;
//記錄當前的Entry
LinkedHashMap.Entry<K,V> current;
//記錄是否發生了迭代過程中的修改
int expectedModCount;
LinkedHashIterator() {
//初始化的時候把head給next
next = head;
expectedModCount = modCount;
current = null;
}
public final boolean hasNext() {
return next != null;
}
//這里采用的是鏈表方式的遍歷方式,有興趣的園友可以去上一章看看HashMap的遍歷方式
final LinkedHashMap.Entry<K,V> nextNode() {
LinkedHashMap.Entry<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
//記錄當前的Entry
current = e;
//直接拿after給next
next = e.after;
return e;
}
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;
}
}
LinkedHashMap遍歷的方式使鏈表,順序訪問的話速度應該會更快一些。
四、總結
在閱讀分析了HashMap的基礎上,看LinkedHashMap會簡單很多,覺得有收獲的園友可以點一下推薦,另外有解讀不對的地方可以留言指正,最后謝謝各位園友觀看,與大家共同進步!
