继承图如下
LHM与HashMap
/*
* LinkedHashMap是有序Map,也是HashMap的子类,其基础结构与HashMap一致
* 这里有序的含义是说其遍历顺序与元素加入的顺序有关
* 该Map中的元素按其加入的顺序,维护一个双向链表,为其额外建立了前后链接
*
* 普通情况下,LinkedHashMap的遍历操作中,元素顺序就是其加入到Map时的顺序
* accessOrder:true:按访问顺序排序(LRU),false:按插入顺序排序;
*/
public class LinkedHashMap<K, V> extends HashMap<K, V> implements Map<K, V>
可以看到LHM以HashMap为父类,维护了一条双向链表来解决插入顺序与遍历顺序的一致性问题
首先参考了https://segmentfault.com/a/1190000012964859的图来讲明LHM的基本结构
可以看到可以在不同哈希桶的链表&红黑树之间有一条双向链表来维护整个hashmap数据的先后顺序。
重要字段
// 是否开启afterNodeAccess()功能
final boolean accessOrder;
/**
* The head (eldest) of the doubly linked list.
*/
// 将加入的结点串成一个链表,head指向表头
transient LinkedHashMap.Entry<K, V> head;
/**
* The tail (youngest) of the doubly linked list.
*/
// 将加入的结点串成一个链表,tail指向表尾
transient LinkedHashMap.Entry<K, V> tail;
如果开启了accessOrder标记,那么元素顺序与每个元素被访问的频率也有关.
重要方法
其实与HashMap相比,增删查改方法大都差不多,不过HashMap的afterNodeAccess
,afterNodeInsertion
,afterNodeRemoval
等方法为空,所以下面只要关注与维护双向链表有关的代码。
插入
LHM并没有重写put和putVal方法,直接用了其父类HashMap的实现,而主要不同在于创建新节点和替换结点的代码:
先来看HashMap的代码:
// 创建一个普通Node
Node<K, V> newNode(int hash, K key, V value, Node<K, V> next) {
return new Node<>(hash, key, value, next);
}
// 创建一个红黑树的TreeNode
TreeNode<K, V> newTreeNode(int hash, K key, V value, Node<K, V> next) {
return new TreeNode<>(hash, key, value, next);
}
// 从红黑树的TreeNode转换为一个普通Node
Node<K, V> replacementNode(Node<K, V> p, Node<K, V> next) {
return new Node<>(p.hash, p.key, p.value, next);
}
// 从普通Node转换为一个红黑树的TreeNode
TreeNode<K, V> replacementTreeNode(Node<K, V> p, Node<K, V> next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}
然后我们来看LHM的代码:
// 创建一个普通Node
Node<K, V> newNode(int hash, K key, V value, Node<K, V> e) {
LinkedHashMap.Entry<K, V> p = new LinkedHashMap.Entry<>(hash, key, value, e);
// 将结点p链接在已有的结点之后,在插入新结点时会用到
linkNodeLast(p);
return p;
}
// 创建一个红黑树的TreeNode
TreeNode<K, V> newTreeNode(int hash, K key, V value, Node<K, V> next) {
TreeNode<K, V> p = new TreeNode<>(hash, key, value, next);
// 将结点p链接在已有的结点之后,在插入新结点时会用到
linkNodeLast(p);
return p;
}
// 从红黑树的TreeNode转换为一个普通Node
Node<K, V> replacementNode(Node<K, V> p, Node<K, V> next) {
LinkedHashMap.Entry<K, V> src = (LinkedHashMap.Entry<K, V>) p;
LinkedHashMap.Entry<K, V> dst = new LinkedHashMap.Entry<>(src.hash, src.key, src.value, next);
// 用dst替换src,在替换结点时候会用到
transferLinks(src, dst);
return dst;
}
// 从普通Node转换为一个红黑树的TreeNode
TreeNode<K, V> replacementTreeNode(Node<K, V> p, Node<K, V> next) {
LinkedHashMap.Entry<K, V> src = (LinkedHashMap.Entry<K, V>) p;
TreeNode<K, V> dst = new TreeNode<>(src.hash, src.key, src.value, next);
// 用dst替换src,在替换结点时候会用到
transferLinks(src, dst);
return dst;
}
总结就是创建新节点之后会调用linkNodeLast()
方法来使得新结点连接在已有结点之后。
而replacement结点之后会调用transferLinks
来调换链表中两个节点位置。我们重点来看这两个方法。
- linkNodeLast 方法
// 将结点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;
}
}
因为一直会维护tail这个引用,就不需要从头遍历了,逻辑也不难
- transferLinks 方法
private void transferLinks(LinkedHashMap.Entry<K, V> src, LinkedHashMap.Entry<K, V> dst) {
// 把src的前驱后继结点赋给dst的前驱后继结点
LinkedHashMap.Entry<K, V> b = dst.before = src.before;
LinkedHashMap.Entry<K, V> a = dst.after = src.after;
if(b == null) {
head = dst;
} else {
// dst->before->after = dst
b.after = dst;
}
if(a == null) {
tail = dst;
} else {
// dst->after->before = dst
a.before = dst;
}
}
删除
同样remove操作复用了父类的方法,那么如何维护双向链表呢?答案是在调用removeNode
之后调用afterNodeRemoval
// 从Map中移除结点e之后,也要解除其在链表上的链接
void afterNodeRemoval(Node<K, V> e) { // unlink
LinkedHashMap.Entry<K, V> p = (LinkedHashMap.Entry<K, V>) e, b = p.before, a = p.after;
// b -> p - > a
p.before = p.after = null;
if(b == null) {
head = a;
} else {
b.after = a;
}
if(a == null) {
tail = b;
} else {
a.before = b;
}
}
逻辑也很简单
访问
// 根据指定的key获取对应的value,如果不存在,则返回null
public V get(Object key) {
// 根据给定的key和hash(由key计算而来)查找对应的(同位)元素,如果找不到,则返回null
Node<K, V> e = getNode(hash(key), key);
if(e == null) {
return null;
}
if(accessOrder) {
afterNodeAccess(e);
}
return e.value;
}
可以看到LHM重写了父类的get方法,如果accessOrder为true,每次访问完某个元素之后会调用afterNodeAccess
,会把访问后的结点移动到链表的末尾,相当于实现了LRU功能
// 访问结点e之后,如果结点e不在表尾,则会将其移动到表尾(该项功能默认是关闭的,由accessOrder负责开启)
void afterNodeAccess(Node<K, V> e) {
LinkedHashMap.Entry<K, V> last;
if(accessOrder && (last = tail) != e) {
//如果能进入到if语句,说明e不是双向链表的末尾,那么a作为p(e强转为Entry为p)的后继不可能为null对吧?
//如果不能进入if语句,说明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;
}
// 那么既然a不可能为null,这个判断是否多余?这里的if else是否多余?
// 其实不是的,如果是多线程的话,可能另外一条线程把a改成了null,那么这里就有可能
// 走到else语句里面了
if(a != null) {
a.before = b;
} else {
last = b;
}
if(last == null) {
head = p;
} else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
LHM as LRU Cache
链表头为最老的元素,如果cache容量大于capacity
// 在插入新结点的同时,移除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);
}
}
// 移除最近最少被访问条件之一,通过覆盖此方法可实现不同策略的缓存
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
举个简单的例子,可以借助LHM写一个LRU的cache,代码参考的是ref
class SimpleLRUCache<K,V> extends LinkedHashMap<K, V> {
private int limit;
public SimpleLRUCache(int limit){
//注意要设置accessOrder为true
super(limit, 0.75f, true);
this.limit = limit;
}
public V save(K key, V val) {
return put(key, val);
}
public V getOne(K key) {
return get(key);
}
public boolean exists(K key) {
return containsKey(key);
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > limit;
}
}
public class UserCase {
public static void main(String[] args) {
SimpleLRUCache<Integer, Integer> cache = new SimpleLRUCache<>(3);
for (int i = 0; i < 10; i++) {
cache.save(i, i * i);
}
System.out.println("插入10个键值对后,缓存内容:");
System.out.println(cache + "\n");
System.out.println("访问键值为7的节点后,缓存内容:");
cache.getOne(7);
System.out.println(cache + "\n");
System.out.println("插入键值为1的键值对后,缓存内容:");
cache.save(1, 1);
System.out.println(cache);
}
}
ref
https://segmentfault.com/a/1190000012964859