我们工作中最常用到的是HashMap,几乎没有用到过LinkedHashMap,LinkedHashMap到底有什么用呢?
用处非常大,如果你被面试到过手写LRU,就会感叹为啥没有早点看到这篇文章。
1. 概述
LinkedHashMap继承自 HashMap,因此具有 HashMap的所有特性。
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
LinkedHashMap内部维护了一个双向链表,用来记录插入顺序或者访问顺序。
// 双链表头结点
transient LinkedHashMap.Entry<K,V> head;
// 双链表尾节点
transient LinkedHashMap.Entry<K,V> tail;
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);
}
}
2. 初始化
public LinkedHashMap() {
// 调用的是父类HashMap的构造方法
super();
// 默认是false,表示按照插入顺序排序,最近插入的移动到链表尾部
// true表示链表按照访问顺序排序,最近访问的移动到链表尾部
accessOrder = false;
}
// 初始化时,可以指定排序方式accessOrder
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
3. 心机的HashMap
在HashMap里面有几个空方法,留给子类去实现。
通过名称就能知道这几个方法是干嘛用的,可见好的命名习惯有多么重要,连注释都省了。
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
// 当一个节点被访问后,做的操作
void afterNodeAccess(Node<K,V> p) { }
// 当新插入一个节点时,做的操作
void afterNodeInsertion(boolean evict) { }
// 当删除一个节点时,做的操作
void afterNodeRemoval(Node<K,V> p) { }
}
这几个空方法,在HashMap里面也被调用了,虽然没有任何操作。当子类LinkedHashMap实现了这些方法,就能起到应有的作用了。
【HashMap调用空方法.png】
4. 实现父类HashMap的空方法
看一下LinkedHashMap是实现这些方法的:
4.1 当一个节点被访问后,移动到链表末尾
void afterNodeAccess(Node<K,V> e) {
LinkedHashMap.Entry<K,V> last;
// 如果设置了按照访问顺序排序,并且当前节点不是尾节点,才进行操作
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.after = null;
// 如果当前节点是头节点,就把当前节点的下个节点设置成头节点
if (b == null)
head = a;
else
// 否则,就断开跟前一个节点的连接
b.after = a;
// 当前节点是尾节点,就把当前节点的前一个节点设置成尾节点
if (a == null)
last = b;
else
// 否则,就断开跟下一个节点的连接
a.before = b;
// 如果只有一个节点,就把当前节点设置成头节点
if (last == null)
head = p;
else {
// 否则,就把当前节点追加到尾节点上
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
4. 2 当一个节点被新插入,删除最旧的节点
void afterNodeInsertion(boolean evict) {
LinkedHashMap.Entry<K,V> first;
// 如果头节点不是null,并且设置了删除最旧节点,就会删除头节点
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
// 是否删除最旧节点,默认是false,初始化LinkedHashMap可以手动设置
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
4.3 删除一个节点
// 删除节点e
void afterNodeRemoval(Node<K,V> e) {
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;
}
5. put方法源码
LinkedHashMap其实是调用的父类HashMap的put的方法,只是覆盖了HashMap新建节点的方法
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;
}
}
6. 实现LRU缓存
由于LinkedHashMap记录了节点插入顺序和访问顺序,新节点或者经常访问的节点被移动末尾,超过固定长度的时候,就删除头节点,这些功能就是为了实现LRU(最近最少访问)缓存量身定做的。
class LRUCache<K, V> extends LinkedHashMap<K, V> {
// 最大长度是3
private static final int MAX_SIZE = 3;
// 超过最大长度,就删除最旧的节点
@Override
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return size() > MAX_SIZE;
}
LRUCache() {
// true表示按访问顺序排序
super(MAX_SIZE, 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");
System.out.println(cache.keySet()); // 输出 [1, 2, 3]
cache.get(2);
// 最近访问的,被移动到末尾
System.out.println(cache.keySet()); // 输出 [1, 3, 2]
lru.put(4, "d");
// 超过最大长度,就删除头节点
System.out.println(lru.keySet()); // 输出 [3, 2, 4]
}
7. 总结
LinkedHashMap相比HashMap,就多了两个功能:
- 用双链表记录了插入顺序和访问顺序
- 超过最大长度,删除链表头节点
在日常开发中,虽然几乎不会用到LinkedHashMap,但是当你手动去实现一个LRU的缓存的时候,必定会用到LinkedHashMap。就算你不用,也一定会参考LinkedHashMap的设计。
面试的时候,也经常遇到面试官让你手写一个LRU,看完了LinkedHashMap源码,再也不用担心不会写了。