LinkedHashMap 是通过哈希表和双向链表来实现的,其基于双向链表来保证对哈希表迭代时的有序性。
LinkedHashMap 继承自 HashMap,从而可以直接复用 HashMap 对哈希表的操作逻辑,其只需要额外维护1套双向链表的操作逻辑即可。
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
{
......
}
节点
LinkedHashMap 的节点 Entry 继承自 HashMap 的节点 Node。
LinkedHashMap.Entry 在 HashMap.Node 的基础上增加了2个指针 before(前向指针) 和 after(后向指针),以用于实现双向链表。
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 没有实现 put() 方法,直接复用 HashMap 的 put() 方法:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
这时候,有读者就会问,LinkedHashMap 不是还需要维护1个双向链表吗?HashMap 的 put() 方法里也没有涉及双向链表的操作呀!
其实,LinkedHashMap 是通过覆写 HashMap 的 newNode() 方法来实现双向链表的。
其调用路径是:
HashMap.put()-->HashMap.putVal()-->LinkedHashMap.newNode()
接着看一下 LinkedHashMap 的 newNode() 方法:
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;
// 若当前链表的尾节点为null,说明该链表为空,将头节点指针也更新到当前节点
if (last == null)
head = p;
else {
// 当前的前向节点为原链表的尾节点
p.before = last;
// 原链表尾节点的后向节点为当前节点
last.after = p;
}
}
其实就是将当前节点连接到当前链表的尾部,为了同时支持红黑树的节点,LinkedHashMap 也覆写了 newTreeNode() 方法:
TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
linkNodeLast(p);
return p;
}
为了能够插入到 LinkedHashMap 的双向链表中,HashMap 的红黑树节点 TreeNode 必然是 LinkedHashMap.Entry 的子类,打开 HashMap 的源码验证一下:
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
TreeNode<K,V> parent; // red-black tree links
TreeNode<K,V> left;
TreeNode<K,V> right;
TreeNode<K,V> prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
...
}
哈哈哈,果不其然。LinkedHashMap 继承自 HashMap,而 HashMap.TreeNode 又是继承自 LinkedHashMap.Entry,所以 LinkedHashMap 和 HashMap 是你中有我,我中有你的关系,名副其实的好基友。
除了 newNode() 和 newTreeNode() 方法,HashMap 的 purVal() 方法步骤里还有2个回调方法值得我们关注,就是 afterNodeAccess() 和 afterNodeInsertion() 方法。
顾名思义,afterNodeAccess() 方法是执行节点访问时回调,afterNodeInsertion() 方法是执行节点插入时访问。
看一下这2方法在 HashMap 中的实现:
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
均是空实现,也就是说 HashMap 在查询或插入元素的时候,相当于这俩回调方法不执行任何操作。
但 LinkedHashMap 却覆写了这2方法,以用来实现某些特殊逻辑。
首先看一下 LinkedHashMap.afterNodeAccess():
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;
}
}
一句话概括就是,当有节点访问操作时,且 accessOrder 设置为 true,会将当前节点移动到双向链表的尾部(若当前节点已经是双向链表的尾部,则无需任何操作)。
换句话说,当 accessOrder 设置为 true 时,越是最近访问过的节点越靠近双向链表的尾部,而很久没有访问过的节点越靠近双向链表的头部,可以看出,LinkedHashMap 可以用于实现 LRU 算法。
接着看一下 LinkedHashMap.afterNodeInsertion():
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);
}
}
afterNodeInsertion() 可能会删除老元素,但需要满足3个条件:
- evict 为 true;
- (first = head) != null,双向链表的头结点不能为 null,换句话说,双向链表中必须有老元素(没有老元素还删个锤锤);
- removeEldestEntry(first) 方法返回为 true。
LinkedHashMap 在执行 put() 操作时,调用的是 HashMap 的 put() 方法,而HashMap 的 put() 方法传入的 evict 为 true,所以第1个条件满足。
第2个条件只要 LinkedHashMap 中存在老元素,也很容易满足,关键是第3个条件:
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
LinkedHashMap.removeEldestEntry() 方法直接返回 false,即 LinkedHashMap 在执行元素插入时,默认是不删除老元素的。
如果想要在元素插入时,执行删除老元素的操作,就必须覆写 removeEldestEntry() 方法。
我们再回顾一下,上述2个方法在 HashMap.putVal() 方法中的位置:
虽然 putVal 属于"增"操作,但如果待插入元素的 key 已经在 Map 中存在时,"增"操作就变成了"改"操作,而除了"查"操作会调用 afterNodeAccess(),"改"操作也会调用 afterNodeAccess() 方法。
“增删改查”!!!
afterNodeAccess() 服务于查和改;
afterNodeInsertion() 服务于增。
谁来服务于删呢?
答案就是 afterNodeRemoval() 方法。
同样的,HashMap.afterNodeRemoval() 也是个空实现。
void afterNodeRemoval(Node<K,V> p) { }
删除元素
与 put() 方法类似,LinkedHashMap 删除元素的操作也直接复用的 HashMap 的 remove() 方法:
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
那如何在哈希表删除元素后,同时在双向链表上删除呢?LinkedHashMap 是通过覆写 HashMap 的 afterNodeRemoval() 方法来实现的:
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;
}
查询元素
LinkedHashMap 直接覆写了 get() 方法用于实现元素查询:
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;
}
其与 HashMap.get() 方法相比,其实就是多了个 afterNodeAccess() 的逻辑。
原则上,与增/删元素类似,afterNodeAccess() 方法也可以直接植入到 HashMap 的 get() 方法中去,由于 HashMap 的 afterNodeAccess() 方法为空实现,所以不会影响 HashMap 的固有逻辑,而 LinkedHashMap 也就不用覆写 get() 方法了,至于 LinkedHashMap 为啥没这么实现,暂时不得而知。
基于LinkedHashMap实现LRU算法
LRU,Least Recently Used 的缩写,即最近最少使用算法,是一种内存数据淘汰策略。使用场景是,当内存不足时,需要淘汰掉缓存中最近最少使用的数据。
缓存一般使用 HashMap 类的数据结构,可以以 O(1) 的复杂度基于 Key 取到对应的 Value,如果在此基础上实现 LRU 算法,则需要额外维护一个链表,当某个数据被访问时,则将该数据移动到链表尾部,这样最近访问到的数据必然存在于链表尾部,而头部位置的数据就是很少被访问到数据,当链表长度到达容量阈值时,可以删除头部数据。
哈哈哈,是不是发现 LinkedHashMap 天然满足上述需求,只需要额外配置2项即可实现 LRU 算法。
- 将 accessOrder 设置为 true,当有节点访问操作时,会将当前节点移动到双向链表的尾部,从而实现最近最少访问的节点处于链表的头部;
- 覆写 LinkedHashMap 的 removeEldestEntry() 方法,使其到达缓存容量阈值的时候,触发删除操作。
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
public class LRULinkedHashMap<K, V> extends LinkedHashMap<K, V> {
// 定义缓存容量
private int capacity;
public LRULinkedHashMap(int capacity) {
// 必须将 accessOrder 参数设置为 True,从而将最近访问的元素移动到链表尾部
super(16, 0.75f, true);
this.capacity = capacity;
}
// 实现 LRU 算法的关键,removeEldestEntry() 方法会在添加元素的时候被调用
// 当 map 中的元素个数大于缓存最大容量,则删除链表的头元素
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
System.out.println("待删除元素: " + eldest.getKey() + "=" + eldest.getValue());
return size()>capacity;
}
public static void main(String[] args) {
Map<Integer, String> map = new LRULinkedHashMap<>(4);
map.put(1, "孙悟空");
map.put(2, "猪八戒");
map.put(3, "哪吒");
map.put(4, "白龙马");
map.put(5, "唐僧");
for(Iterator<Map.Entry<Integer,String>> it = map.entrySet().iterator(); it.hasNext();){
Map.Entry<Integer, String> next = it.next();
System.out.println(next.getKey() + "=" + next.getValue());
}
map.get(2);
map.put(6, "杨戬");
for(Iterator<Map.Entry<Integer,String>> it = map.entrySet().iterator(); it.hasNext();){
Map.Entry<Integer, String> next = it.next();
System.out.println(next.getKey() + "=" + next.getValue());
}
// 更新操作也会将更新的节点移动到链表尾部
map.put(4, "太白金星");
System.out.println("################################");
for(Iterator<Map.Entry<Integer,String>> it = map.entrySet().iterator(); it.hasNext();){
Map.Entry<Integer, String> next = it.next();
System.out.println(next.getKey() + "=" + next.getValue());
}
}
}
程序运行结果如下:
待删除元素: 1=孙悟空
待删除元素: 1=孙悟空
待删除元素: 1=孙悟空
待删除元素: 1=孙悟空
待删除元素: 1=孙悟空
2=猪八戒
3=哪吒
4=白龙马
5=唐僧
待删除元素: 3=哪吒
4=白龙马
5=唐僧
2=猪八戒
6=杨戬
################################
5=唐僧
2=猪八戒
6=杨戬
4=太白金星
一切符合预期,完美收工。
本文到此结束,感谢阅读!