歡迎關注個人公衆號「彤哥讀源碼」,查看更多源碼系列文章, 與彤哥一塊兒暢遊源碼的海洋。java
LinkedHashMap內部維護了一個雙向鏈表,能保證元素按插入的順序訪問,也能以訪問順序訪問,能夠用來實現LRU緩存策略。node
LinkedHashMap能夠當作是 LinkedList + HashMap。git
LinkedHashMap繼承HashMap,擁有HashMap的全部特性,而且額外增長的按必定順序訪問的特性。數組
咱們知道HashMap使用(數組 + 單鏈表 + 紅黑樹)的存儲結構,那LinkedHashMap是怎麼存儲的呢?緩存
經過上面的繼承體系,咱們知道它繼承了Map,因此它的內部也有這三種結構,可是它還額外添加了一種「雙向鏈表」的結構存儲全部元素的順序。ide
添加刪除元素的時候須要同時維護在HashMap中的存儲,也要維護在LinkedList中的存儲,因此性能上來講會比HashMap稍慢。性能
/** * 雙向鏈表頭節點 */
transient LinkedHashMap.Entry<K,V> head;
/** * 雙向鏈表尾節點 */
transient LinkedHashMap.Entry<K,V> tail;
/** * 是否按訪問順序排序 */
final boolean accessOrder;
複製代碼
(1)headthis
雙向鏈表的頭節點,舊數據存在頭節點。spa
(2)tailcode
雙向鏈表的尾節點,新數據存在尾節點。
(3)accessOrder
是否須要按訪問順序排序,若是爲false則按插入順序存儲元素,若是是true則按訪問順序存儲元素。
// 位於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用於雙向鏈表存儲全部元素。
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緩存策略的關鍵。
在節點插入以後作些什麼,在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,驅逐的意思。
(1)若是evict爲true,且頭節點不爲空,且肯定移除最老的元素,那麼就調用HashMap.removeNode()把頭節點移除(這裏的頭節點是雙向鏈表的頭節點,而不是某個桶中的第一個元素);
(2)HashMap.removeNode()從HashMap中把這個節點移除以後,會調用afterNodeRemoval()方法;
(3)afterNodeRemoval()方法在LinkedHashMap中也有實現,用來在移除元素後修改雙向鏈表,見下文;
(4)默認removeEldestEntry()方法返回false,也就是不刪除元素。
在節點訪問以後被調用,主要在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;
}
}
複製代碼
(1)若是accessOrder爲true,而且訪問的節點不是尾節點;
(2)從雙向鏈表中移除訪問的節點;
(3)把訪問的節點加到雙向鏈表的末尾;(末尾爲最新訪問的元素)
在節點被刪除以後調用的方法。
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;
}
複製代碼
經典的把節點從雙向鏈表中刪除的方法。
獲取元素。
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()方法把訪問的節點移到雙向鏈表的末尾。
(1)LinkedHashMap繼承自HashMap,具備HashMap的全部特性;
(2)LinkedHashMap內部維護了一個雙向鏈表存儲全部的元素;
(3)若是accessOrder爲false,則能夠按插入元素的順序遍歷元素;
(4)若是accessOrder爲true,則能夠按訪問元素的順序遍歷元素;
(5)LinkedHashMap的實現很是精妙,不少方法都是在HashMap中留的鉤子(Hook),直接實現這些Hook就能夠實現對應的功能了,並不須要再重寫put()等方法;
(6)默認的LinkedHashMap並不會移除舊元素,若是須要移除舊元素,則須要重寫removeEldestEntry()方法設定移除策略;
(7)LinkedHashMap能夠用來實現LRU緩存淘汰策略;
LinkedHashMap如何實現LRU緩存淘汰策略呢?
首先,咱們先來看看LRU是個什麼鬼。LRU,Least Recently Used,最近最少使用,也就是優先淘汰最近最少使用的元素。
若是使用LinkedHashMap,咱們把accessOrder設置爲true是否是就差很少能實現這個策略了呢?答案是確定的。請看下面的代碼:
package com.coolcoding.code;
import java.util.LinkedHashMap;
import java.util.Map;
/** * @author: tangtong * @date: 2019/3/18 */
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;
}
}
複製代碼
歡迎關注個人公衆號「彤哥讀源碼」,查看更多源碼系列文章, 與彤哥一塊兒暢遊源碼的海洋。