linkedhashmap 排序_JDK1.8 util-LinkedHashMap源码分析

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

fa72f89ca37df958b27d1057e816f234.png

1概述

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

2.1 LinkedHashMap的数据结构

781e2af54d152049a11ab2bf98aafb0d.png

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

2.2 LinkedHashMap的继承关系

2.2.1 Entry的继承关系

616024bd470ac364b31b06dd0c03b86e.png

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

3LinkedHashMap源码解析

本节我们将结合HashMap的部分源码一起分析一下LinkedHashMap。

c3ae7e9dce812791c25c68af7638f398.png

构造方法

跟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函数进行操作,这里有一个非常精彩的设计

c9f0c4fd71980d8cb0fac433c8647068.png

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

bb4460ad15fdd3b67dc473997a9c80e5.png

如果该lhp设置访问顺序排序为真的话,那么每次访问的K-V都需要放在双向链表的尾端

第二个标红的函数 afterNodeInsertion是lhm设计的另一个精彩的地方:

b445c3067767fd02f113bc05ee788e71.png

这个函数,做了如下的事:如果给双向链表本身一个节点上限,那么随着数据增长的时候,超过上限后,每次新插入的节点仍然放在尾结点,但是头节点就被干掉了,即维护了一个“最近被插入”的数据结构

6 LinkedHashMap的remove()

remove操作与put操作基本一致 也是利用多态性

8c221abb067be96cae1a8f18798a8c11.png

4521ed0dbea14f9bfaca25ae4bd92105.png

这个函数就做了一件事,将该节点冲双向链表中 解耦 ,里面充斥着这样小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的源码

99fdc300b64f08c2456acf81def5ef8e.png
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值