
1概述
LinkedHashMap 继承自 HashMap,在 HashMap 基础上,通过维护一条双向链表,解决了 HashMap 不能随时保持遍历顺序和插入顺序一致的问题。除此之外,LinkedHashMap 对访问顺序也提供了相关支持。在一些场景下,该特性很有用,比如缓存。在实现上,LinkedHashMap 很多方法直接继承自 HashMap,仅为维护双向链表覆写了部分方法。所以,要看懂 LinkedHashMap 的源码,需要先看懂 HashMap 的源码。
2.1 LinkedHashMap的数据结构

可以从上图中看到,LinkedHashMap数据结构相比较于HashMap来说,添加了双向指针,分别指向前一个节点——before和后一个节点——after,从而将所有的节点已链表的形式串联一起来,从名字上来看LinkedHashMap与HashMap有一定的联系,实际上也确实是这样,LinkedHashMap继承了HashMap,重写了HashMap的一部分方法,从而加入了链表的实现。让我们来看一下它们的继承关系。
2.2 LinkedHashMap的继承关系
2.2.1 Entry的继承关系

核心数据结构中,lhm在自己的Node结构中,加入了before与after节点,表明 这是一个双向链表,那么如果尾结点的after指向了head,head的before指向了tail节点,那么这就是一个循环链表,通过代码发现并没有这么说,表明这仅仅是一个双向链表;与Node中的next不同,next描述的是经过hash后,同一个桶中(即下标)的链表节点关系,这也说明,进行节点相关操作的时候,需要注意(也没什么注意的,因为在父类中,已经对next节点进行了调整,子类只要解决自己的事就可以了,这也是为什么用继承的原因了)
3LinkedHashMap源码解析
本节我们将结合HashMap的部分源码一起分析一下LinkedHashMap。

构造方法
跟HashMap类似的构造方法这里就不一一赘述了,里面唯一的区别就是添加了前面提到的accessOrder,默认赋值为false——按照插入顺序来排列,这里主要说明一下不同的构造方法。
//多了一个 accessOrder的参数,用来指定按照LRU排列方式还是顺序插入的排序方式
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}4 LinkedHashMap的get()方法
public V get(Object key) {
Node<K,V> e;
//调用HashMap的getNode的方法,详见上一篇HashMap源码解析
if ((e = getNode(hash(key), key)) == null)
return null;
//在取值后对参数accessOrder进行判断,如果为true,执行afterNodeAccess
if (accessOrder)
afterNodeAccess(e);
return e.value;
}从上面的代码可以看到,LinkedHashMap的get方法,调用HashMap的getNode方法后,对accessOrder的值进行了判断,我们之前提到:
accessOrder为true则表示按照基于访问的顺序来排列,意思就是最近使用的entry,放在链表的最末尾
由此可见,afterNodeAccess(e)就是基于访问的顺序排列的关键,让我们来看一下它的代码:
//此函数执行的效果就是将最近使用的Node,放在链表的最末尾
void afterNodeAccess(Node<K,V> e) {
LinkedHashMap.Entry<K,V> last;
//仅当按照LRU原则且e不在最末尾,才执行修改链表,将e移到链表最末尾的操作
if (accessOrder && (last = tail) != e) {
//将e赋值临时节点p, b是e的前一个节点, a是e的后一个节点
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
//设置p的后一个节点为null,因为执行后p在链表末尾,after肯定为null
p.after = null;
//p前一个节点不存在,情况一
if (b == null) // ①
head = a;
else
b.after = a;
if (a != null)
a.before = b;
//p的后一个节点不存在,情况二
else // ②
last = b;
//情况三
if (last == null) // ③
head = p;
//正常情况,将p设置为尾节点的准备工作,p的前一个节点为原先的last,last的after为p
else {
p.before = last;
last.after = p;
}
//将p设置为将p设置为尾节点
tail = p;
// 修改计数器+1
++modCount;
}
}5 LinkedHashMap的put()方法
增加节点:lhm本身并没有实现put 操作,而是通过hm的put函数进行操作,这里有一个非常精彩的设计

截图是hm中put函数的实现,这两个标红函数在hm本身是空回调,而在lhp中,分别进行了实现

如果该lhp设置访问顺序排序为真的话,那么每次访问的K-V都需要放在双向链表的尾端
第二个标红的函数 afterNodeInsertion是lhm设计的另一个精彩的地方:

这个函数,做了如下的事:如果给双向链表本身一个节点上限,那么随着数据增长的时候,超过上限后,每次新插入的节点仍然放在尾结点,但是头节点就被干掉了,即维护了一个“最近被插入”的数据结构
6 LinkedHashMap的remove()
remove操作与put操作基本一致 也是利用多态性


这个函数就做了一件事,将该节点冲双向链表中 解耦 ,里面充斥着这样小trick 不用的变量,即时null;
7 LinkedHashMap的迭代器
abstract class LinkedHashIterator {
//记录下一个Entry
LinkedHashMap.Entry<K,V> next;
//记录当前的Entry
LinkedHashMap.Entry<K,V> current;
//记录是否发生了迭代过程中的修改
int expectedModCount;
LinkedHashIterator() {
//初始化的时候把head给next
next = head;
expectedModCount = modCount;
current = null;
}
public final boolean hasNext() {
return next != null;
}
//这里采用的是链表方式的遍历方式,有兴趣的园友可以去上一章看看HashMap的遍历方式
final LinkedHashMap.Entry<K,V> nextNode() {
LinkedHashMap.Entry<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
//记录当前的Entry
current = e;
//直接拿after给next
next = e.after;
return e;
}
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount;
}
}LinkedHashMap遍历的方式使链表,顺序访问的话速度应该会更快一些。
总结
当我们基于 LinkedHashMap 实现缓存时,通过覆写removeEldestEntry方法可以实现自定义策略的 LRU 缓存。比如我们可以根据节点数量判断是否移除最近最少被访问的节点,或者根据节点的存活时间判断是否移除该节点等。
欢迎各位关注我个人的公众号,会不定期更新jdk的源码

本文深入剖析LinkedHashMap,它继承HashMap并维护双向链表,保证遍历顺序。通过分析构造方法、get()、put()、remove()以及迭代器,揭示其内部工作原理,尤其在访问顺序排序上的实现。LinkedHashMap适用于实现LRU缓存等场景。
1577

被折叠的 条评论
为什么被折叠?



