首页 > 编程语言 >LinkedHashMao & LinkedHashSet源码阅读

LinkedHashMao & LinkedHashSet源码阅读

时间:2024-01-17 18:35:31浏览次数:24  
标签:LinkedHashMao LinkedHashSet HashMap after 插入 源码 Entry 节点 LinkedHashMap

目录

本人的源码阅读主要聚焦于类的使用场景,一般只在java层面进行分析,没有深入到一些native方法的实现。并且由于知识储备不完整,很可能出现疏漏甚至是谬误,欢迎指出共同学习

本文基于corretto-17.0.9源码,参考本文时请打开相应的源码对照,否则你会不知道我在说什么

简介

LinkedHashMap继承于HashMap,在看本篇之前需要先看过HashMap & HashSet源码阅读。相比HashMap,LinkedHashMap增加了一个特性:迭代顺序与插入顺序相同,没别的。

模型

这个小节主要介绍如何做到在HashMap的基础上,让元素的迭代顺序与他们的插入顺序相同。其实主要是修改了节点,在HashMap.Node的基础上,LinkedHashMap添加了两个指针before和after,before指向上一个被插入的节点,after指向下一个插入的节点:

LinkedHashMap_base.png

千万别将before/after和next/prev这两对指针搞混了,前者是将哈希表所有节点按照插入的顺序依次连接起来,形成一个全局的链表;而后者是将桶中的节点连接起来,形成一个局部的链表。

LinkedHashMap只需要维护一个指针tail,指向最新插入哈希表的节点,在下次插入的时候,采用尾插法将其链到tail之后。

而对于迭代,LinkedHashMap还需再维护一个指针head,指向最老插入哈希表的节点,从这个节点开始遍历这个全局链表,就能做到“迭代顺序与插入顺序相同”。

明白了模型之后,代码分析其实也就很简单。

代码分析

成员变量

public class LinkedHashMap<K,V>
  extends HashMap<K,V>
  implements Map<K,V>
{
  static class Entry<K,V> extends HashMap.Node<K,V> {
    // before指向上一个插入哈希表的节点,after指向下一个插入哈希表的节点
    Entry<K,V> before, after;
    Entry(int hash, K key, V value, Node<K,V> next) {
      super(hash, key, value, next);
    }
  }
	// 指向最新插入哈希表的节点
  transient java.util.LinkedHashMap.Entry<K,V> head;
  // 指向最老插入哈希表的节点
  transient java.util.LinkedHashMap.Entry<K,V> tail;
  // 按照插入顺序还是访问顺序迭代
  final boolean accessOrder;
}

除了accessOrder外,在模型都提到过,不再赘述。在模型中说的是“迭代顺序与插入顺序相同”,其实LinkedHashMap还支持“迭代顺序与访问顺序相同”,所谓“访问”指的是key在哈希表已经存在,get或put都算访问,当然,访问模式下插入新节点的话,与插入模式的行为相同。两种模式的区别在于,插入模式下访问已经存在的key不会改变其迭代顺序。

在开始分析实现前,可以猜一下访问模式是如何实现的。我猜应该是将被访问的节点从链表断开,然后重新插入到链表尾...

方法

构造方法没什么可介绍的,直接看核心方法。核心方法是在HashMap中定义的三个钩子函数:

void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }

之前在介绍HashMap的方法时候,可以看到put、remove等方法实现里调用了这三个方法。这是一种名为 模版模式 的设计模式:定义一个操作中的算法的骨架(这里是HashMap的put、get等),而将一些步骤延迟到子类中(钩子函数)。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

通过「模型」这个小节可以知道,LinkedHashMap要维护一个全局链表(节点是LinkedHashMap.Entry),因此这三个方法就是为了在增删改查节点后用来更新全局链表。由于比较简单,下面一次性通过代码注释解析:

// 节点被删除后
void afterNodeRemoval(HashMap.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;
  // 更新head和被删节点前驱的after
  if (b == null)
    head = a;
  else
    b.after = a;
  // 更新head和被删节点后继的before
  if (a == null)
    tail = b;
  else
    a.before = b;
}
// 新节点插入后
void afterNodeInsertion(boolean evict) {
  LinkedHashMap.Entry<K,V> first;
  // 移除最老的节点,是否移除与removeEldestEntry有关
  if (evict && (first = head) != null && removeEldestEntry(first)) {
    K key = first.key;
    removeNode(hash(key), key, null, false, true);
  }
}
// 节点访问后
void afterNodeAccess(HashMap.Node<K,V> e) {
  LinkedHashMap.Entry<K,V> last;
  // 如果是访问模式并且访问的不是尾节点,那么要将该节点从全局链表断开,并重新插入链表尾
  if (accessOrder && (last = tail) != 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;
    if (a != null)
      a.before = b;
    else
      last = b;
    if (last == null)
      head = p;
    else {
      p.before = last;
      last.after = p;
    }
    tail = p;
    ++modCount;
  }
}

值的注意的是afterNodeInsertion中有一个删除最老节点的操作,这个是为了实现LRU/FIFO缓存算法。首先LRU删除的是最老的(最久未被使用)节点,并且LinkedHashMap确实也支持找到最老的节点,因此多了这个“删除最老节点”的操作,但LinkedHashMap默认是不会删除最老节点的,也就是removeEldestEntry返回false,这样就跟普通的Map一样,存多少就是多少:

protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
  return false;
}

用户可以继承LinkedHashMap,并重写这个方法来实现一个KV缓存类,比如下面实现了一个基于LRU策略的键值对缓存(代码来自com.mysql.cj.util.LRUCache,随手拿来作为例子):

public class LRUCache<K, V> extends LinkedHashMap<K, V> {
  protected int maxElements;
	// maxSize为哈希表可存储最多的键值对,当键值对超过maxSize时,最老的键会被自动删除
  public LRUCache(int maxSize) {
    super(maxSize, 0.75F, true); // 注意第三个参数传accessOrder传true,否则就变成FIFOCache了
    this.maxElements = maxSize;
  }

  protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
    return this.size() > this.maxElements;
  }
}

至于LinkedHashMap的其他方法没什么好分析,迭代器呢,很明显就是从head开始顺着after往后遍历,也没啥好讲,不过迭代性能是比HashMap好点的,因为HashMap是遍历整个table,相当于遍历所有桶,包括空桶。而LinkedHashMap是遍历全局链表,迭代时间就只跟size(实际节点的个数)相关,而跟table.length无关。

补充 - LinkedHashSet

参考HashMap & HashSet源码阅读中介绍的HashSet与HashMap的关系。

总结

LinkedHashMap在HashMap的基础上,根据节点插入/访问的先后顺序将所有节点用双指针串起来,实现了迭代顺序与插入/访问的顺序相同的特性。另外,因为LinkedHashMap可以直接获取到最老的节点,因此进一步支持用户通过继承LinkedHashMap实现键值对缓存。

参考链接

「博客园」HashMap & HashSet源码阅读

「Java全栈知识体系」Map - LinkedHashSet&Map源码解析

标签:LinkedHashMao,LinkedHashSet,HashMap,after,插入,源码,Entry,节点,LinkedHashMap
From: https://www.cnblogs.com/nosae/p/17970705

相关文章

  • 掌上医院预约挂号源码,uni-app+.net公众号、支付宝小程序预约挂号平台
    线上预约挂号系统构建了医院和患者的连接,通过改善患者院内的就医服务流程,以公众号、支付宝小程序为服务入口,为居民提供导诊、预约、支付、报告查询等线上线下一体化的就医服务,缩短患者就诊环节,提高医疗机构服务效率。●与医院HIS系统深度融合,实现诊疗业务及数据线上线下的双向传......
  • SpringSecurity系列,第四章:源码分析
    源码分析SpringSecurity的核心功能即为:认证(Authentication)、授权(Authorization)一、概览1、在SpringSecurity中,用户的认证信息主要由Authentication的实现类来保存,Authentication接口定义如下:【保存用户认证信息】publicinterfaceAuthenticationextendsPrin......
  • HashMap & HashSet源码阅读
    目录简介模型代码分析成员变量方法参考链接本人的源码阅读主要聚焦于类的使用场景,一般只在java层面进行分析,没有深入到一些native方法的实现。并且由于知识储备不完整,很可能出现疏漏甚至是谬误,欢迎指出共同学习本文基于corretto-17.0.9源码,参考本文时请打开相应的源码对照,否则......
  • 【送酒小程序系统源码】/花店送花系统/蛋糕店系统/奶茶店系统源码
    前端uniapp+后端thinkphp+数据库mysql多门店外卖餐饮点餐系统预约点餐匹配附近店铺 堂食外卖带走  菜品管理.根据用户的位置匹配附近饭店 点餐后,可以在线等叫号餐时输入手机号并支付后,可以支持外支持多规格、备注等快捷功能,以吸多多门店管理 数据概览支持微信小程序 ......
  • 尚无忧【无人共享空间 saas 系统源码】无人共享麻将室系统源码共享自习室系统源码,共享
    可saas多开,非常方便,大大降低了上线成本UNIAPP+thinkphp+mysql独立开源!1、定位功能:可定位附近是否有店2、能通过关键字搜索现有的店铺3、个性轮播图展示,系统公告消息提醒4、个性化功能展示,智能排序,距离、价格排序5、现有店铺清单展示,订房可查看房间单价,根据日期、时间端订房,选择时......
  • PDF转图片-itextpdf-java源码
    提供PDF文件转图片的工具类。电子签章过程中存在着在网页上对签署文件进行预览、指定签署位置、文件签署等操作,由于图片在浏览器上的兼容性和友好性优于PDF文件,所以一般在网页上进行电子签章时,会先将PDF文件转换成图片,展示给用户。用户在页面上确定好签署位置,并进行签署时,后端服......
  • PDF转图片-itextpdf-java源码
    提供PDF文件转图片的工具类。电子签章过程中存在着在网页上对签署文件进行预览、指定签署位置、文件签署等操作,由于图片在浏览器上的兼容性和友好性优于PDF文件,所以一般在网页上进行电子签章时,会先将PDF文件转换成图片,展示给用户。用户在页面上确定好签署位置,并进行签署时,后......
  • 基于SpringBoot+Vue的校园招聘系统设计实现(源码+lw+部署文档+讲解等)
    (文章目录)前言:heartpulse:博主介绍:✌全网粉丝10W+,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战✌:heartpulse:......
  • C#微信公众号HIS预约挂号系统源码
    微信公众号预约挂号系统、支付宝小程序预约挂号系统主要是让自费、医保患者在手机上就能实现就医全过程,实时预约挂号、自费、医保结算,同时还可以查询检查检验报告等就诊信息,真正实现了让信息“多跑路”,让群众“少跑腿”。系统与HIS对接,通过医院微信公众号,患者用身份证注册以后,可以......
  • 【深入挖掘Java技术】「源码原理体系」盲点问题解析之HashMap工作原理全揭秘(上)
    知识盲点概念介绍HashMap是基于Map接口构建的数据结构,它以键值对的形式存储元素,允许键和值都为null。由于键的唯一性,HashMap中只能有一个键为null。HashMap的特点是元素的无序性和不重复性。注意,HashMap并不是线程安全的。在多线程环境下,如果不进行适当的同步处理,可能会导致数据不......