文章目录
LinkHashMap数据结构分析
LinkHashMap相对HashMap,记录了元素的插入顺序,在遍历时,可以根据元素的插入顺序进行反问,LinkHashMap的顺序是基于双向链表实现的,因而LinkHashMap同时具备栈和队列的基础功能。
数据结构如下:
LinkedHashMap会将元素串起来,形成一个双链表结构。可以看到,其结构在HashMap结构上增加了链表结构。数据结构为(数组 + 单链表 + 红黑树 + 双链表),图中的标号是结点插入的顺序。注意,在图中未展现出红黑树的特征,这个可以参照HashMap的数据结构。
类定义
下面先看看LinkedHashMap的基本成员属性,具体源码如下:
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V> {
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);
}
}
// 链表头结点
transient LinkedHashMap.Entry<K,V> head;
// 链表尾结点
transient LinkedHashMap.Entry<K,V> tail;
// 访问顺序,如果为false,表示根据插入顺序进行访问
// 如果为true表示之后访问顺序按照元素的访问顺序进行,不按照之前的插入顺序。
final boolean accessOrder;
}
从源码中看到,LinkedHashMap中定义了Entry内部类,继承自HashMap.Node节点,具备Node的相关特性,同时还定义了Entry<K,V> before, after;
,根据插入顺序用于索引遍历成员元素,在LinkedHashMap内部还定义了head和tail成员属性,用于记录整个双向链表的收尾节点,用于实现双向遍历。
LinkedHashMap继承自HashMap,具有HashMap的全部特性,下面结合源码分析LinkedHashMap的双向链表特性
linkNodeLast 插入双链表尾部
LinkedHashMap重写了HashMap的newNode和newTreeNode方法:
// 当桶中结点类型为HashMap.Node类型时,调用此函数
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
// 生成Node结点
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(hash, key, value, e);
// 将该结点插入双链表末尾
linkNodeLast(p);
return p;
}
// 当桶中结点类型为HashMap.TreeNode时,调用此函数
TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
// 生成TreeNode结点
TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
// 将该结点插入双链表末尾
linkNodeLast(p);
return p;
}
在生成新节点后,通过linkNodeLast建立维护新节点的双向链表关系。linkNodeLast的实现较为简单,主要用于更新tail节点为新节点
// link at the end of list
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
// 记录原来的尾节点
LinkedHashMap.Entry<K,V> last = tail;
// 记录新尾节点为p
tail = p;
if (last == null)
// 仅第一次插入或元素清空后,原来的尾节点为空,此时同时记录首节点
head = p;
else {
// 建立原来尾节点和新尾节点的双向链表关系
p.before = last;
last.after = p;
}
}
afterNodeAccess 节点访问回调,设为尾节点
回调时机
在HashMap中,很多操作后都会回调这个方法,主要聚焦在需要访问而非移除这个节点的时候,除此之外,还有一个核心使用是在LinkedHashMap重写的get方法中:
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
// 如果获取是空节点,直接返回
return null;
if (accessOrder)
// 如果accessOrder为true,回调
afterNodeAccess(e);
return e.value;
}
从上面看到可见仅有accessOrder为true时,且访问节点不等于尾节点时,在每次获取该节点时,都会回调。
用途原理分析
afterNodeAccess的主要用途是将访问的节点移到链表尾部,可以借此实现LRU(Least recently used,最近最少使用)算法
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
// 若访问顺序为true,且访问的对象不是尾结点
if (accessOrder && (last = tail) != e) {
// 向下转型,记录p的前后结点
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
// p的后结点为空,因为p要移动到尾部
p.after = null;
// 维护->关系,如果p的前结点为空
if (b == null)
// 说明p是头节点,将p的后一节点a设为头结点
head = a;
else // p的前结点不为空
// b的后结点为a,即移除对p的引用
b.after = a;
// 维护<-关系,p的后结点不为空
if (a != null)
// a的前结点为b
a.before = b;
else // p的后结点为空,p为尾节点,记录last为p的前一个节点
last = b;
// last=tail || last=b,分析last=null,则p为唯一有效节点
if (last == null)
// 记录头节点为p
head = p;
else { // p链入最后一个结点后面
p.before = last;
last.after = p;
}
// 尾结点为p
tail = p;
// 增加结构性修改数量
++modCount;
}
}
以上函数的意义在于,加入链式关系为a->b->c->d,访问b后,会变成a->c->d->b。
afterNodeRemoval 节点移除回调
afterNodeRemoval在HashMap#removeNode中移除节点成功后回调,从双向链表关系中移除该节点。
void afterNodeRemoval(Node<K,V> e) { // unlink
// 从链表中移除节点
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
// 清空p的双向引用
p.before = p.after = null;
// 没有前置节点,设头节点为p的后置节点
if (b == null)
head = a;
else
// 更新前置节点b对p的后置引用关系
b.after = a;
// 没有后置节点,设尾节点为p的后置节点
if (a == null)
tail = b;
else
// 更新后置节点a对p的前置引用关系
a.before = b;
}
afterNodeInsertion 节点插入回调
afterNodeInsertion用于处理新元素插入后的回调,如果入参evict=true、存在元素且满足removeEldestEntry移除规则,则删除头部元素。
具体实现如下:
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
// 如果入参evict=true、存在元素且满足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中,removeEldestEntry默认返回false,即不移除元素,我们可以在子类中拓展这个方法实现对头部元素的移除,如实现LRU算法时使用。
entrySet 遍历元素
遍历时,主要关注遍历顺序的相关实现:
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
}
final class LinkedEntrySet extends AbstractSet<Map.Entry<K,V>> {
public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
if (action == null)
throw new NullPointerException();
// 缓存修改记录
int mc = modCount;
// 通过after进行顺序遍历
for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
action.accept(e);
// 并发修改检测
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
从上面看到,遍历时是从head开始,调用LinkedHashMap.Entry的after引用来实现顺序遍历。