【学习笔记】JDK源码学习之LinkedHashMap(附带面试题)

news2025/1/22 12:56:49

【学习笔记】JDK源码学习之LinkedHashMap(附带面试题)

其他好文: 地址

什么是 LinkedHashMap ? 它的作用又是什么?它和 HashMap 有什么区别呢?

老样子,带着以上问题来深入了解 LinkedHashMap 的作用吧。

1、什么是LinkedHashMap?

LinkedHashMap 继承于 HashMap ,在 HashMap 的基础上,通过维护一条双向链表,解决了 HashMap 不能随时保持遍历顺序和插入顺序一致性的问题。

继承图:

在这里插入图片描述

源码:

public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>
{
  ...
}

从源码中我们能发现 LinkedHashMap 是继承了 HashMap 且实现了 Map 这个接口。

如果大家还没有看过 HashMap 的一些详细介绍,可以参考本篇文章嗷:地址

也就是说 HashMap 的一些特性也是被 LinkedHashMap 继承了下来。

  • 可以有序(插叙),不可重复,允许 null 键值对的存在。
  • 也是一个 KV 的结构。
  • 同样也是单线程安全·,多线程会出现相对应的问题。

但是 LinkedHashMap 真的就和 HashMap 没有什么区别了吗?

这个问题我们先不回答,大家可以慢慢的向下继续寻找答案。

2、LinkedHashMap的常用变量、构造函数和常用方法

2.1 LinkedHashMap的常用变量

源码:

    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);
        }
    }

    private static final long serialVersionUID = 3801124242820219131L;

    /**
     * The head (eldest) of the doubly linked list.
     */
    transient LinkedHashMap.Entry<K,V> head;

    /**
     * The tail (youngest) of the doubly linked list.
     */
    transient LinkedHashMap.Entry<K,V> tail;

    /**
     * The iteration ordering method for this linked hash map: <tt>true</tt>
     * for access-order, <tt>false</tt> for insertion-order.
     *
     * @serial
     */
    final boolean accessOrder;
  • Node : LinkedHashMap 重写了HashMap的Node内部类,新增了 before , after 两个属性来记录头节点和尾结点。
  • head :链表的头节点。
  • tail : 链表的尾节点。
  • accessOrder :是否开启 LRU 算法(后面会讲到)。

HashMap 的链表是单链表,而这里的链表是双向链表。

HashMap:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3eUajPLF-1671604054110)(/Users/tiejiaxiaobao/Library/Application Support/typora-user-images/image-20221217135148008.png)]

LinkedHashMap:

这就是两者的的区别之一,剩下的我们可以继续向下看嗷。

2.2 LinkedHashMap 中的构造函数

源码:

    public LinkedHashMap() {
        super();
        accessOrder = false; // 是否开启LRU缓冲
    }

    public LinkedHashMap(int initialCapacity) {
        super(initialCapacity);
        accessOrder = false; // 是否开启LRU缓冲
    }

    public LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
        accessOrder = false; // 是否开启LRU缓冲
    }

    public LinkedHashMap(Map<? extends K, ? extends V> m) {
        super();
        accessOrder = false; // 是否开启LRU缓冲
        putMapEntries(m, false);
    }
    public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }


共有五种构造方法:

  • LinkedHashMap() :无参构造
  • LinkedHashMap(int initialCapacity) : 有参构造,initialCapacity 表示为初始化容量。
  • LinkedHashMap(int initialCapacity, float loadFactor) 有参构造,loadFactor 表示为 扩容的加载因子
  • LinkedHashMap(Map<? extends K, ? extends V> m) 有参构造,m 表示用另外一个 map 的所有数据到本map中。
  • LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) : 有参构造,accessOrder 表示为 是否开启 LRU 一般默认都是打开 false

2.3 LinkedHashMap中常用的方法

LinkedHashMap 中常用的方法有以下几种:

  • get
  • put
  • remove

我们就用以上三种来进行逐一的分析。

2.3.1 get方法

先看看源码:

LinkedHashMap :

    public V get(Object key) {
        Node<K,V> e;
        // 调用 hashMap 中的 getNode() 方法,根据 key 的哈希值找到对应的桶位置,判断节点后(链表、头结点、树节点)进行返回
        if ((e = getNode(hash(key), key)) == null)
            return null;
        // 如果 accessOrder 为 true,获取元素后把当前键值对调整到尾部
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
    }

虽说 LinkedHashMap 继承了 HashMap ,但是有一些方法 LinkedHashMap 也是进行了重写,去来符合 LinkedHashMap 的构造。

get 就是重写了 HashMap 中的 get(Object key) 方法。

主要就是增加了 accessOrder 相关操作。核心的查找操作还是通过 HashMap 中的方法去进行。

补充:

afterNodeAccess 方法:

    void afterNodeAccess(Node<K,V> e) { // move node to last
        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;
            // 把当前节点的 after 节点置 null
            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;
        }
    }

我们以下图举例看下整个 afterNodeAccess 过程是是怎么样的,比如我们该次操作访问的是 13 这个节点,而 14 是其后驱,11 是其前驱,且 tail = 14 。在通过 get 访问 13 节点后, 13变成了 tail 节点,而14变成了其前驱节点,相应的 14的前驱变成 11 ,11的后驱变成了14, 14的后驱变成了13.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6jBKtG1C-1671604054110)(/Users/tiejiaxiaobao/Library/Application Support/typora-user-images/image-20221217195053916.png)]

2.3.2 put方法

  • LinkedHashMap 并没有重写任何 put() 方法,但是其重写了构建新节点的 newNode() 方法。
  • newNode() 会在 HashMapputVal() 方法里被调用,putVal() 方法会在批量插入数据 putMapEntries(Map<? extends K, ? extends V> m, boolean evict) 或者插入单个数据 public V put(K key, V value) 时被调用。

源码

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

// 说明:在这里再次强调一遍:evict参数用于LinkedHashMap中的尾部操作,这里没有实际意义。
// onlyIfAbsent参数用于是否覆盖相同key下的value值
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
  			// 首先判断是否需要扩容
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
        	// 这里调用的是LinkedHashMap的newNode()方法。
        	// 如果理解多态的:这点应该很容易理解
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
          	// 判断当前节点是否是树
            else if (p instanceof TreeNode)
              	// 遍历红黑树进行添加
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                    	// 同上
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
               // 由LinkedHashMap的实现,并调用
               // 作用:
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        // 由LinkedHashMap的实现,并调用
        // 作用:在执行一次插入操作都会执行的操作
        // 主要就是对LRU算法的支持。
        // 是否移动最早的元素。但是LinkedHashMap中总是返回false.所以在这里没什么用。
        afterNodeInsertion(evict);
        return null;
    }

    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;
    }

    // link at the end of list
    private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
        LinkedHashMap.Entry<K,V> last = tail;
       // p为新的需要追加的结点
        tail = p;
        // 如果last为null.则表示现在链表为空。新new出来的p元素就是链表的头结点
        if (last == null)
            head = p;
        else {
        // 否则就是链表中已存在结点的情况:往尾部添加即可
        	// 把新追加p的结点的前驱结点设置之前的尾部结点
        	// 把之前的尾部结点的后驱结点设为新追加的p结点
            p.before = last;
            last.after = p;
        }
    }


在LinkedHashMap类使用的仍然是父类HashMap的put方法,所以插入节点对象的流程基本一致。不同的是,LinkedHashMap重写了afterNodeInsertionafterNodeAccess方法。

afterNodeInsertion方法用于移除链表中的最旧的节点对象,也就是链表头部的对象。但是在JDK1.8版本中,可以看到removeEldestEntry一直返回false,所以该方法并不生效。如果存在特定的需求,比如链表中长度固定,并保持最新的N的节点数据,可以通过重写该方法来进行实现。

2.3.3 remove

LinkedHashMap 重写了 afterNodeRemoval 方法,用于在删除节点的时候,调整双链表的结构。

源码:

     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;
        }
    }

		// 双向链表的删除
    void afterNodeRemoval(Node<K,V> e) { // unlink
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        p.before = p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;
        if (a == null)
            tail = b;
        else
            a.before = b;
    }


3、总结

  1. LinkedHashMap的实现还是比较简单的,是LinkedList+HashMap。拥有HashMap的特性并维持了双向链表保存存储顺序或访问顺序。老数据存在前面,新数据尾部插入。
  2. LinkedHashMap的实现几乎都是实现了HashMap未实现的空方法,实现HashMap的钩子方法复用HashMap的方法。这种父类暴露钩子函数子类实现的方式可以在后续开发实现。
  3. LinkedHashMap在节点添加前后指向实现元素存储/访问,用空间换时间。
  4. 两种访问方式:访问/新增

4、常见面试题

  1. LinkedHashMap是怎么保证元素有序的?
  2. LinkedHashMap和HashMap的有什么异同点?
  3. LinkedHashMap的在设置时用到了哪些Java的思想或设计模式?
  4. LinkedHashMap是个什么东西?
  5. LinkedHashMap在使用上有啥特点?
  6. LinkedHashMap访问有序是怎么体现的呢?是直接调用get()方法就会自动排序么?
  7. LinkedHashMap的双向链表对象都包含什么属性?
  8. LinkedHashMap调用remove()后链表怎么维护?

答案地址: 答案地址

参考文章

https://juejin.cn/post/6844903590159450120#heading-6

https://blog.csdn.net/codejas/article/details/85471109

https://blog.csdn.net/weixin_39723544/article/details/83269282

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/105577.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

音频文件格式有哪些?误删音频文件如何恢复?

音频用于在计算机系统上以数字格式存储的音频数据。日常生活中&#xff0c;我们也会通过录音来保存一些重要的内容&#xff0c;或者是记录一些重要的事情。但是&#xff0c;你知道音频文件有哪几种格式吗&#xff1f;如果音频在保存的过程中&#xff0c;因为我们的误操作&#…

72. 如何给 SAP ABAP ALV 报表的修改功能添加自定义校验逻辑

本教程前面的步骤,我们已经介绍了如何编辑 SAP ALV 报表里的某一列数据: 70. 利用 ALV 实现增删改查系列之二:仅让 ALV 报表某一列允许被编辑如下图 Airfare 和 Capacity 列所示。然而实际的开发项目中,我们肯定不会允许用户对这些列进行随心所欲的修改,必定要增添一些自定…

力扣(202.454)补9.18

202.快乐数 不会。又要用到数学的思想&#xff0c;你要学会去分析。&#x1f641; 根据上表你需要知道&#xff0c;一个很大的数&#xff0c;经过快乐运算&#xff0c;不可能会一直增大&#xff0c;一定会受到限制。 class Solution { private int getNext(int n) { …

知识点21--springboot 文件上传

前面有一篇简单版的文件上传&#xff0c;是为了让大家知道文件上传是在干什么&#xff0c;但是在正式的开发中文件上传是一个稍微有些麻烦的东西&#xff0c;需要从页面层开发到数据层&#xff0c;如果你常常听人说文件上传会知道有一些相关的名词&#xff0c;比如切片、秒传、…

编程算法集锦

编程算法集锦一、分治法1.分治法介绍2.归并排序3.快速排序4.中值问题二、贪心法1.贪心法2.最小生成树Kruskal算法3.Huffman编码4.单源点最短路径三、回溯法1.回溯法-n皇后问题2.子集和数四、动态规划1.数塔问题2.最长公共子序列3.求序列-2 11 -4 13 -5 -2的最大字段和4.求最长的…

Linux内核工作队列(workqueue)详解

1、为什么需要工作队列&#xff1f; 在内核代码中&#xff0c;经常会遇到不能或不合适去马上调用某个处理过程&#xff0c;此时希望将该工作推送给某个内核线程执行&#xff0c;这样做的原因有很多&#xff0c;比如&#xff1a; 中断触发了某个过程的执行条件&#xff0c;而该过…

电表485通讯抄表软件

电表485通讯主要是有线抄表&#xff0c;电表485通讯抄表软件选用485线传送数据&#xff0c;适宜集中化安装电表&#xff0c;下列给您具体说说电表485通讯抄表原理、应用领域等。 电表485通讯抄表原理 RS485抄表适用电表集中化安装场合&#xff0c;为节省RS485通讯线成本&…

VR渲染之Stereo Rendering解析

VR渲染的独特和最明显的方面之一是需要生成两个视图&#xff0c;左右眼睛各一个。我们需要这两个视图来为观众创建立体3D效果。 Multi Camera 传统上&#xff0c;VR应用程序必须绘制两次几何体--一次是左眼&#xff0c;一次是右眼。这基本上使非VR应用程序所需的处理翻了一番。…

揭秘百度智能测试在测试定位领域实践

作者 | intelligents 前几篇&#xff0c;分别介绍了测试活动测试输入、测试执行、测试分析、测试定位和测试评估五个步骤中测试输入、执行、分析、评估的智能化研究和实践&#xff0c;本章节重点介绍测试定位环节的智能化实践。 测试定位的主要作用是在构建失败或问题发生后&…

傻白探索Chiplet,国内外研究现状(六)

目录 一、概述 二、国外Chiplet历史与现状 2.1 AMD 2.1.1 EPYC&#xff08;Naples&#xff09; 2.1.2 EPYC&#xff08;Rome&#xff09; 2.1.3 EPYC&#xff08;Milan-X &#xff09; 2.1.4 Ryzen&#xff08;Matisse&#xff09; 2.2 苹果 2.3 Intel 2.3.1 Alter…

【大数据技术】Spark+Flume+Kafka实现商品实时交易数据统计分析实战(附源码)

需要源码请点赞关注收藏后评论区留言私信~~~ Flume、Kafka区别和侧重点 1&#xff09;Kafka 是一个非常通用的系统&#xff0c;你可以有许多生产者和消费者共享多个主题Topics。相比之下&#xff0c;Flume是一个专用工具被设计为旨在往HDFS&#xff0c;HBase等发送数据。它对H…

2022年我国江蓠行业现状:养殖面积、产量不断增长 进口量仍大于出口

根据观研报告网发布的《中国江蓠市场现状深度研究与发展前景预测报告&#xff08;2022-2029年&#xff09;》显示&#xff0c;江蓠属于“海藻”产业&#xff0c;为暖水性藻类&#xff0c;我国俗称 “龙须菜”、 “海菜”、 “蚝菜”。藻体紫褐色或紫黄色、绿色。 江蓠在热带、 …

Opencv(C++)笔记--霍夫变换检测直线、霍夫变换检测圆

目录 1--原理 2--Opencv API 3--实例代码 4--霍夫变换检测圆 1--原理 具体原理可参考 博客1 和 视频讲解1&#xff1b; 霍夫变换检测直线的核心思想是&#xff1a;在笛卡尔坐标系下&#xff0c;一条直线&#xff08;两个点&#xff08;x1, y1&#xff09;和&#xff08;x2,…

行业权威来揭秘,商用PC为什么首选12代酷睿

第12代酷睿处理器可以提供更卓越的性能&#xff0c;凭借架构先进性让商用台式机和笔记本电脑为用户带来更好的体验&#xff0c;帮助企业和员工效率倍增。 作者|九月 来源| PConline 想要让办公效率进一步提升&#xff0c;一台强大的PC设备是必不可少的生产力和内容创作工…

有什么适合零基础的人做的副业兼职

互联网上有很多套路。这是不可预防的。只要你敢贪婪&#xff0c;你就会陷入别人设计的陷阱。在业余时间做兼职应该是很多人的梦想&#xff0c;因为他们可以在有限的时间内赚更多的钱。很多人不知道的是&#xff0c;其实我们赚钱的渠道很多:比如网上发文章.短视频直播.我们媒体、…

基于SpringBoot+Mybatis框架的私人影院预约系统(附源码,包含数据库文件)

基于SpringBootMybatis框架的私人影院预约系统&#xff0c;附源码&#xff0c;包含数据库文件。 非常完整的一个项目&#xff0c;希望能对大家有帮助哈。 本系统的完整源码以及数据库文件都在文章结尾处&#xff0c;大家自行获取即可。 项目简介 该项目设计了基于SpringBoo…

Spring MVC—Spring MVC概述

文章目录Java web的发展历史一.Model I和Model II1.Model I开发模式2.Model II开发模式二. MVC模式SpringMVC 的工作原理和流程springmvc 的拦截器Spring和SpringMVC的区别————————————————————————————————Java web的发展历史 一.Model I和M…

VS Code debug调试时无法查看变量内容【已解决】

问题场景&#xff1a;新换成的vscode编译软件&#xff0c;但是在debug调试时发现与QtCreator不同&#xff0c;无法直接查看变量&#xff0c;显示的都是地址或其他。 比如&#xff1a;QString或QStringList无法查看具体的内容&#xff0c;正常是这样显示的&#xff0c;反正我不…

Linux神器——vim

目录 一、vim基本概念 二、vim基本操作 三、vim正常模式命令集 四、vim末行模式命令集 五、vim操作总结 六、vim界面配置 vi/vim的区别简单点来说&#xff0c;它们都是多模式编辑器&#xff0c;不同的是vim是vi的升级版本&#xff0c;它不仅兼容vi的所有指令&#xff0c;而…

上班15年后,普通程序员能实现财富自由吗?

对于职业生涯还没有开挂的普通程序员来说&#xff0c;有可能实现财务自由吗&#xff1f; 先来说下财务自由的最低标准 北上广深&#xff1a;身价3000万&#xff0c;含房产1000万、现金2000万。 杭州、南京、成都等二线城市&#xff1a;身价1500万&#xff0c;含500万房产、现…