上一篇文章笔者对Collection包中非常重要的一个类
HashMap
进行了分析和总结, 如果你对HashMap
的知识点有模糊, 建议先读一读JDK1.8 Collection知识点与代码分析–HashMap但是HashMap
存在一些问题, 最重要的是HashMap不能够保证存储元素的有序性, 这一点是因为HashMap的遍历是按照桶顺序的, 而节点的插入顺序和桶的顺序无关, 并且还会在resize的时候进一步打乱. 那有没有既能够在常数时间进行增删改查又能够保持有序的Map呢, 于是就有了LinkedHashMap
.
LinkedHashMap
是HashMap
的子类, 所以其落脚点是HashMap
, 在其基础上, 通过一个双向链表将节点按照插入顺序或访问顺序连接起来, 就能够以较低的成本达到上述的有序的目标.
同时, 因为其实现了按照访问顺序的排序, 所以也是天然的LRU容器, 官方的文档中就有建议, 可以用它来作为LRU缓存, 按照一定的规则删除最老的节点, 例如当size超过一个值时, 删除最长时间未访问的元素. 在文章的最后, 笔者也实现了一个基于LinkedHashMap
的LRU管理的demo. 另一方面, 因为是HashMap
的子类,LinkedHashMap
也不能保证线程安全, 如果需要进行并发, 需要使用它的synchronized
的装饰版本. 本文的所有源码分析都是基于JDK1.8版本.
Constructor
LinkedHashMap
和HashMap
的构造器基本一致, 但是多了accessOrder
参数, 当它为false
时, 按照链表按照插入顺序排序, 当它为true时, 链表按照访问顺序排序
// 其他构造器都无法传入accessOrder参数
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
对元素的put, putIfAbsent, get, getOrDefault, compute , computeIfAbsent, computeIfPresent, merge操作(假设元素存在)都视作对元素的访问, replace只有元素被替换的时候才视作一次访问, putAll方法对传入的每个元素产生一次访问, 访问的顺序取决于输入的Map的iterator提供的顺序.
Entry
Entry类是HashMap.Node
的子类, 它在Node
的基础上增加了两个成员变量, before
和after
. 因此Entry
类中有三个指针: before
和after
分别指向双向链表的前一个节点和后一个节点, 而next
指向桶中的链的下一个节点.
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);
}
}
因此, LinkedHashMap
的结构示意图大致是这样,
接下来的问题就是, 添加和删除节点时link是如何处理的, 以及, 当节点被访问时, 在链表中的顺序调整是怎样实现的, 以及, 如果要用它实现LRU应当怎么做.
添加和删除
putVal
LinkedHashMap
没有重写put
方法, 而是利用HashMap
中putVal
的良好设计, 对afterNodeAccess
和afterNodeInsertion
两个回调函数以及newNode
方法进行重写.
我们首先回顾一下HashMap.putVal
方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 懒惰加载初始化
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null); // 注意这里不是new Node( )
else {
// 在桶的链表或树中查找key, 如果不存在也是调用newNode方法创建
// 如果超过阈值, 进行树化
}
if (e != null) { // 如果key存在
...
afterNodeAccess(e);
return oldValue;
}
}
// 递增modCount, 并判断是否扩容
afterNodeInsertion(evict);
return null;
}
注意这里的newNode
函数, 是设计模式中的工厂模式
, 有普通Node和TreeNode两个版本, 在HashMap
中, 内部就是简单的调用了new
产生一个新的实例返回, 但是LinkedHashMap
通过重写这两个函数, 除了返回一个新对象, 还将这个对象添加到链表尾.
此外, LinkedHashMap
重写了afterNodeInsertion
回调函数, 判断是否要将链表表头的元素删除(删除最老的节点), 重写了afterNodeAccess
回调函数, 如果是按照访问顺序排序的话, 将被访问的节点移动到链表的末尾.
注意在JDK1.8以前, 这两个函数分别是Entry.recordAccess
和addEntry
, 1.8把这两个函数的逻辑部分移到putVal中, 而抽象出两个回调函数, 供子类重写, 逻辑上更加的清晰, 也符合面向对象中的开闭原则(开放扩展, 关闭修改).
removeNode
在remove
操作中, 类似的, HashMap
提供了afterNodeRemoval
回调函数, 而LinkedHashMap
重写了它, 做了双向链表的删除节点操作.
按照访问顺序排序
之前提到了afterAccess
回调函数中, 实现了将被访问节点移动到队尾的操作, 源码也就是对双向链表的一个操作.
void afterNodeAccess(Node<K,V> e) { // move node to last
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)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
然后在`HashMap中, 在下面这些函数中, 去调用这个回调函数就能够实现将最近被访问的节点安排在队尾.
将最久没有访问的节点删除
上面提到, 每次插入时, 就会调用afterNodeInsertion
这个回调函数, 因此LinkedHashMap
通过重写这个函数, 来判断是否将最不长访问的节点删除(队首的节点)
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);
}
}
注意removeEldestEntry(first)
函数就是判断第一个节点是否应该被删除的条件, 这个函数在源码中默认为返回false的.
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
重写它, 你就能获得力量就能够用来控制LRU策略. 所以最后, 我们一起来写一个demo用LinkedHashMap
来手写一个LRU.
LinkedHashMap 手写LRU
public class MyLRUSample {
public static void main(String[] args) {
LinkedHashMap<Integer, String> LRU
= new LinkedHashMap<Integer, String>(16, 0.75F, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, String> eldest) {
return size() > 4; // 当缓存超过4个时, 删除最不长访问的
}
};
LRU.put(1, "1");LRU.put(2, "2");LRU.put(3, "3");LRU.put(4, "4");
System.out.println("Original order in LRU");
LRU.forEach((k,v)->System.out.println(k + ":" + v));
LRU.get(3);LRU.get(4);LRU.put(2, "Two");LRU.get(3);
System.out.println("After some accesses, the order in LRU");
LRU.forEach((k,v)->System.out.println(k + ":" + v));
LRU.put(5, "5");
System.out.println("1 has been removed.");
LRU.forEach((k,v)->System.out.println(k + ":" + v));
}
}
输出:
Original order in LRU
1:1
2:2
3:3
4:4
After some accesses, the order in LRU
1:1
4:4
2:Two
3:3
1 has been removed.
4:4
2:Two
3:3
5:5
结果和预期一致, 同时注意到, 重写过的iterator
的访问顺序是从链表头到链表尾的.