JDK版本jdk1.8.0_191 1
从继承体系的角度,LinkedHashMap是HashMap的子类
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
内部维护一个双向链表,维护插入顺序或者LRU顺序。
/**
* The head (eldest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> head;
/**
* The tail (youngest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> tail;
还有一个字段为accessOrder
表示的是链表维护的是插入顺序
还是LRU顺序
。
默认false表示维护插入顺序,而true则表示LRU顺序。
final boolean accessOrder;
那么它内部是怎么维护链表的顺序的呢?
我们先回顾一下HashMap源码解读中的put,get方法,在putVal方法中有两个函数调用
:
afterNodeAccess(e);
afterNodeInsertion(evict);
从字面的意思上可以知道,afterNodeAccess是节点被访问后的函数调用,afterNodeInsertion是节点被插入后的函数调用;而这两个函数实际上在HashMap中是空实现
实际上,类似的函数总共有3个,它们都是空实现
// Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }
HashMap函数中对3个函数注释可以知道,这3个函数是空实现,并且它们是一种回调机制,特地提供给LinkedHashMap来后序回调用的;
所以LinkedHashMap继承自HashMap,一定重写了HashMap的这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.before = p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a == null)
tail = b;
else
a.before = b;
}
// 插入节点后调用
// 当 removeEldestEntry() 方法返回 true 时会移除最晚的节点
// 也就是链表首部节点 first。
// removeEldestEntry() 默认为 false,
// 如果需要让它为 true,需要继承 LinkedHashMap 并且覆盖这个方法的实现,
// 这在实现 LRU 的缓存中特别有用,通过移除最近最久未使用的节点,
// 从而保证缓存空间足够,并且缓存的数据都是热点数据。
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);
}
}
// 节点被访问后调用
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
// 如果accessOrder为true,也就是维护LRU链表
// 如果指定维护LRU,当前访问的节点不在尾部
// 则将此节点移动到尾部
if (accessOrder && (last = tail) != e) {
// ↓
// ... -> b -> e(p) -> a -> ...
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
// 首先after置为空,表示自己会被移动到尾部
p.after = null;
// 将p在链表处中断了的位置接起来
if (b == null) head = a;
else b.after = a;
if (a != null) a.before = b;
else last = b;
if (last == null) head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
这里要注意第二个方法中对removeEldestEntry(first)方法的解读。主要能够实现减少LRU对空间的巨大消耗。下面是实现的一个简单LRU。
// Reference to cyc2018
class LRUCache<K, V> extends LinkedHashMap<K, V> {
private static final int MAX_ENTRIES = 3;
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > MAX_ENTRIES;
}
LRUCache() {
super(MAX_ENTRIES, 0.75f, true);
}
}
public static void main(String[] args) {
LRUCache<Integer, String> cache = new LRUCache<>();
cache.put(1, "a");
cache.put(2, "b");
cache.put(3, "c");
cache.get(1);
cache.put(4, "d");
System.out.println(cache.keySet());
}
/** 运行结果
[3, 1, 4]
*/