LinkedHashMap——基本结构
很明显,LinkedHashMap直接继承了HashMap;
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
{
/**
* 多了一个头指针和尾指针
*/
transient LinkedHashMap.Entry<K,V> head;
transient LinkedHashMap.Entry<K,V> tail;
/**
* 很明显继承了原有Entry的属性,如果要实现按照插入和遍历顺序一样的需求
* 不改变原有的属性,添加两个before、after
* LinkedHashMap的node重名为Entry
*/
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);
}
}
/**
* 重写了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;
}
```java
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
/**
* @hashMap只有一个节点(last == null)
* 直接操作头等于新节点
*/
LinkedHashMap.Entry<K,V> last = tail;
tail = p;
if (last == null)
head = p;
/**
* @否则将末尾节点和新节点调整链表结构
* 操作新节点的前一个节点是末尾节点(p是原末尾节点)
* 操作旧末尾节点的后一个节点等新节点
* 其实就是新节点放最后,形成双向链表
*/
else {
p.before = last;//操作新节点的前一个节点是末尾节点
last.after = p;//操作旧末尾节点的后一个节点等新节点
}
}
HashMap原有的方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
……
/*
针对于key是null的重写了newNode
*/
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
……
/*
在链表中找到了key相同的值进行替换时,激活访问模式的回调
*/
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
/**
激活LRU模式的回调
*/
afterNodeInsertion(evict);
return null;
}
/**
* 这是一个生命周期方法,为了实现扩展
* 原有的是空实现,LinkedHashMap重写了这些,我们下面逐个讲解这些方法
*/
void afterNodeAccess(Node<K,V> p) { }//访问模式专用的链表调整
void afterNodeInsertion(boolean evict) { }//LRU模式专用的链表调整
void afterNodeRemoval(Node<K,V> p) { }//删除时的的链表调整
LinkedHashMap——遍历
public final void forEach(Consumer<? super K> action) {
if (action == null)
throw new NullPointerException();
int mc = modCount;
/*
遍历起始条件是:e首先=head;
遍历终止条件是:e==null;
遍历自增条件是:e.after,选择下一个;
*/
for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
action.accept(e.key);
if (modCount != mc)
throw new ConcurrentModificationException();
}
通过上面的案例,我们明显的看出在node数据结构中的before和after就是实现有序遍历的关键;那么具体在插入节点的时候是如何实现的呢?
LinkedHashMap——删除原理
删除肯定要将断层相连,所以怎么实现的呢?
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;
/**
* @原节点是头节点(前继节点是null)
* 头节点直接等于后继
* 否则前继的后继向后相连
*/
if (b == null)
head = a;
else
b.after = a;
/**
* @原节点是尾节点(后继节点是null)
* 否则前继的后继相连向前相连
*/
if (a == null)
tail = b;
else
a.before = b;
}
LinkedHashMap——访问模式原理
访问模式下,访问元素和插入都会影响链表的双向链表顺序的,这是怎么设计出来的呢?(非访问模式,就是按照插入的时候的linkNodeLast决定顺序即可)
final boolean accessOrder;
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
/*
当元素被访问到后就会进行一次重新的before和after编排
*/
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
……
/*
在链表中找到了key相同的值进行替换时
进行一次重新的before和after编排
*/
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
return null;
}
/**
* @滞末处理
* 更改节点至尾部,显然要处理之前节点把断层补上
*/
void (Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
/**
* @条件开启了accessOrder访问顺序模式且如果旧节点不是末尾节点才进入滞末处理
* last指向末尾节点
*/
if (accessOrder && (last = tail) != e) {
/**
* e代表旧的刚被替换的节点,p是强转后的e,兼容hashMap的方法原型
* b代表被替换的前继,a代表被替换的后继
*/
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.after = null;//即将被调整后最后,所以最后肯定是null但是指针已经在前面保留了
/**
* @去除旧节点连接两个节点中间的断层
*/
/**
* @(1)旧节点是头节点(没前继,前继是空)
* 直接头结点指向自己的后继
*/
if (b == null)
head = a;
else //否则旧节点的前继节点向后连接旧节点的后继节点
b.after = a;
/**
* @(2)旧节点是中间节点(后有继不是空)
* 直接旧节点的后继向前连接旧节点的前继,刚好上面相反
*/
if (a != null)
a.before = b;
else //否则曾经是最后一个节点,将last指向前继,继续处理
last = b;
/**
* @(3)处理只有一个节点时,last也是空(last指向前继是空或者tail是空)
*/
if (last == null)
head = p;
else {
/**
* 这里算是一个兜底策略,可能有两个情况
* 自己是中间节点,last就是tali,做一下指针调整
* 自己是最后一个节点,last是节点的前继节点,就是上面那个情况,曾经是最后一个节点,所以前继就是倒数二个,做一下这两个的指针调整
*/
p.before = last;
last.after = p;
}
tail = p;//调整整体指针
++modCount;
}
}
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
/**
* @此次实现LRU算法
* 需要子类继承LinkHashMap实现removeEldestEntry方法,该方法传入头结点以及某种逻辑决定
* 是否移除头节点,因为头节点意味着经常不被访问
*/
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
LinkedHashMap——LRU算法实现
在插入的时候有这么一个回调处理,
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
……
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
LinkedHashMap的LRU方案
需要继承LinkedHashMap并实现removeEldestEntry方法的逻辑
也就是LinkedHashMap默认不支持LRU,removeEldestEntry默认返回false
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
/**
* @此次实现LRU算法
* 需要子类继承LinkHashMap实现removeEldestEntry方法,该方法传入头结点以及某种逻辑决定
* 是否移除头节点,因为头节点意味着经常不被访问
*/
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;
}
LinkedHashMap——总结
双向链表:node被包成entry多了一个before和after;然后在整体多了两个头指针和尾指针;
普通模式:只是在插入的时候更新尾部节点和新节点的指针关系
访问模式:除此以为get方法也会影响节点的顺序调整,这一切都通过afterNodeAccess回调实现;afterNodeAccess方法还有在插入的时候替换旧元素的时候也会激活;
LRU模式:默认一直不实现,就是删除头节点;需要继承LinkedHashMap重写LRU算法的实现决定true和false来实现整个
GodSchool 致力于简洁的知识工程,输出高质量的知识产出,我们一起努力 博主私人微信:supperlzf