Java容器之LinkedHashMap原始碼分析
一、簡介
LinkedHashMap
內部維護了一個雙向連結串列,能保證元素按插入的順序訪問,也能以訪問順序訪問,可以用來實現LRU
快取策略。
LinkedHashMap
可以看成是LinkedList + HashMap
。
二、繼承體系
LinkedHashMap
繼承HashMap
,擁有HashMap
的所有特性,並且額外增加了按一定順序訪問的特性。
三、儲存結構
我們知道HashMap
使用(陣列 + 單鏈表/紅黑樹)的儲存結構,那LinkedHashMap
是怎麼儲存的呢?
通過上面的繼承體系,我們知道它繼承了HashMap
,所以它的內部也有這三種結構,但是它還額外添加了一種“雙向連結串列”的結構儲存所有元素的順序。
新增刪除元素的時候需要同時維護在HashMap
中的儲存,也要維護在LinkedList
中的儲存,所以效能上來說會比HashMap
稍慢。
四、原始碼解析
4.1 屬性
/**
* 雙向連結串列頭節點
*/
transient LinkedHashMap.Entry<K,V> head;
/**
* 雙向連結串列尾節點
*/
transient LinkedHashMap.Entry<K,V> tail;
/**
* 是否按訪問順序排序
*/
final boolean accessOrder;
-
head
:雙向連結串列的頭節點,舊資料存在頭節點。 -
tail
:雙向連結串列的尾節點,新資料存在尾節點。 -
accessOrder
:是否需要按訪問順序排序,如果為false
則按插入順序儲存元素,如果是true
則按訪問順序儲存元素。
4.2 內部類
// 位於LinkedHashMap中 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); } } // 位於HashMap中 static class Node<K, V> implements Map.Entry<K, V> { final int hash; final K key; V value; Node<K, V> next; }
儲存節點,繼承自HashMap
的Node
類,next
用於單鏈表儲存於桶中,before
和after
用於雙向連結串列儲存所有元素。
4.3 構造方法
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
public LinkedHashMap() {
super();
accessOrder = false;
}
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super();
accessOrder = false;
putMapEntries(m, false);
}
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
前四個構造方法accessOrder
都等於false
,說明雙向連結串列是按插入順序儲存元素。
最後一個構造方法accessOrder
從構造方法引數傳入,如果傳入true
,則就實現了按訪問順序儲存元素,這也是實現LRU
快取策略的關鍵。
4.4 afterNodeInsertion(boolean evict)方法
在節點插入之後做些什麼,在HashMap
中的putVal()
方法中被呼叫,可以看到HashMap
中這個方法的實現為空。
void afterNodeInsertion(boolean evict) { // possibly remove eldest
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;
}
evict
,驅逐的意思。
- 如果
evict
為true
,且頭節點不為空,且確定移除最老的元素,那麼就呼叫HashMap.removeNode()
把頭節點移除(這裡的頭節點是雙向連結串列的頭節點,而不是某個桶中的第一個元素); -
HashMap.removeNode()
從HashMap
中把這個節點移除之後,會呼叫afterNodeRemoval()
方法; -
afterNodeRemoval()
方法在LinkedHashMap
中也有實現,用來在移除元素後修改雙向連結串列,見下文; - 預設
removeEldestEntry()
方法返回false
,也就是不刪除元素。
4.5 afterNodeAccess(Node<K,V> e)方法
在節點訪問之後被呼叫,主要在put()
已經存在的元素或get()
時被呼叫,如果accessOrder
為true
,呼叫這個方法把訪問到的節點移動到雙向連結串列的末尾。
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
// 如果accessOrder為true,並且訪問的節點不是尾節點
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
// 把p節點從雙向連結串列中移除
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
// 把p節點放到雙向連結串列的末尾
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
// 尾節點等於p
tail = p;
++modCount;
}
}
- 如果
accessOrder
為true
,並且訪問的節點不是尾節點; - 從雙向連結串列中移除訪問的節點;
- 把訪問的節點加到雙向連結串列的末尾;(末尾為最新訪問的元素)
4.6 afterNodeRemoval(Node<K,V> 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;
if (b == null)
head = a;
else
b.after = a;
if (a == null)
tail = b;
else
a.before = b;
}
經典的把節點從雙向連結串列中刪除的方法。
4.7 get(Object key)方法
獲取元素。
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;
}
如果查詢到了元素,且accessOrder
為true
,則呼叫afterNodeAccess()
方法把訪問的節點移到雙向連結串列的末尾。
五、總結
-
LinkedHashMap
繼承自HashMap
,具有HashMap
的所有特性; -
LinkedHashMap
內部維護了一個雙向連結串列儲存所有的元素; - 如果
accessOrder
為false
,則可以按插入元素的順序遍歷元素; - 如果
accessOrder
為true
,則可以按訪問元素的順序遍歷元素; -
LinkedHashMap
的實現非常精妙,很多方法都是在HashMap
中留的鉤子(Hook
),直接實現這些Hook
就可以實現對應的功能了,並不需要再重寫put()
等方法; - 預設的
LinkedHashMap
並不會移除舊元素,如果需要移除舊元素,則需要重寫removeEldestEntry()
方法設定移除策略; -
LinkedHashMap
可以用來實現LRU
快取淘汰策略;
六、拓展
LinkedHashMap如何實現LRU快取淘汰策略呢?
首先,我們先來看看LRU
是個什麼鬼。LRU(Least Recently Used)
最近最少使用,也就是優先淘汰最近最少使用的元素。
如果使用LinkedHashMap
,我們把accessOrder
設定為true
是不是就差不多能實現這個策略了呢?答案是肯定的。請看下面的程式碼:
package com.coolcoding.code;
import java.util.LinkedHashMap;
import java.util.Map;
public class LRUTest {
public static void main(String[] args) {
// 建立一個只有5個元素的快取
LRU<Integer, Integer> lru = new LRU<>(5, 0.75f);
lru.put(1, 1);
lru.put(2, 2);
lru.put(3, 3);
lru.put(4, 4);
lru.put(5, 5);
lru.put(6, 6);
lru.put(7, 7);
System.out.println(lru.get(4));
lru.put(6, 666);
// 輸出: {3=3, 5=5, 7=7, 4=4, 6=666}
// 可以看到最舊的元素被刪除了
// 且最近訪問的4被移到了後面
System.out.println(lru);
}
}
class LRU<K, V> extends LinkedHashMap<K, V> {
// 儲存快取的容量
private int capacity;
public LRU(int capacity, float loadFactor) {
super(capacity, loadFactor, true);
this.capacity = capacity;
}
/**
* 重寫removeEldestEntry()方法設定何時移除舊元素
* @param eldest
* @return
*/
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
// 當元素個數大於了快取的容量, 就移除元素
return size() > this.capacity;
}
}