LinkedHashMap源码分析

news2024/12/23 8:29:01

特性

在 HashMap 基础上维护一条双向链表
支持遍历时会按照插入顺序有序进行迭代。LinkedHashMap 的迭代顺序是和插入顺序一致的,这一点是 HashMap 所不具备的。
。支持按照元素访问顺序排序,适用于封装 LRU 缓存工具。
因为内部使用双向链表维护各个节点,所以遍历时的效率和元素个数成正比,相较于和容量成正比的 HashMap 来说,迭代效率会高很多。

结构图

在 HashMap 基础上在各个节点之间维护一条双向链表,使得原本散列在不同 bucket 上的节点、链表、红黑树有序关联起来。
在这里插入图片描述

LRU 缓存

通过 LinkedHashMap 我们可以封装一个简易版的 LRU(Least Recently Used,最近最少使用) 缓存,确保当存放的元素超过容器容量时,将最近最少访问的元素移除。
在这里插入图片描述
具体实现思路如下:

  • 继承 LinkedHashMap;
  • 构造方法中指定 accessOrder 为 true ,这样在访问元素时就会把该元素移动到链表尾部,链表首元素就是最近最少被访问的元素;
  • 重写removeEldestEntry 方法,该方法会返回一个 boolean 值,告知 LinkedHashMap 是否需要移除链表首元素(缓存容量有限)。
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private final int capacity;

    public LRUCache(int capacity) {
        super(capacity, 0.75f, true);
        this.capacity = capacity;
    }

    /**
     * 判断size超过容量时返回true,告知LinkedHashMap移除最老的缓存项(即链表的第一个元素)
     */
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > capacity;
    }
}

为什么 HashMap 的树节点 TreeNode 要通过 LinkedHashMap 获取双向链表的特性呢?

我们都知道 LinkedHashMap 是在 HashMap 基础上对节点增加双向指针实现双向链表的特性,所以 LinkedHashMap 内部链表转红黑树时,对应的节点会转为树节点 TreeNode,为了保证使用 LinkedHashMap 时树节点具备双向链表的特性,所以树节点 TreeNode 需要继承 LinkedHashMap 的 Entry。

为什么不直接在 Node 上实现前驱和后继指针呢?

我们直接在 HashMap 的节点 Node 上直接实现前驱和后继指针,然后 TreeNode 直接继承 Node 获取双向链表的特性为什么不行呢?其实这样做也是可以的。只不过这种做法会使得使用 HashMap 时存储键值对的节点类 Node 多了两个没有必要的引用,占用没必要的内存空间

初始化

如果我们要让 LinkedHashMap 实现键值对按照访问顺序排序(即将最近未访问的元素排在链表首部、最近访问的元素移动到链表尾部),需要调用第 4 个构造方法将 accessOrder 设置为 true。

get

public V get(Object key) {
    Node < K, V > e;
    //获取key的键值对,若为空直接返回
    if ((e = getNode(hash(key), key)) == null)
        return null;
    //若accessOrder为true,则调用afterNodeAccess将当前元素移到链表末尾
    if (accessOrder)
        afterNodeAccess(e);
    //返回键值对的值
    return e.value;
}

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;
        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;
    }
}
  1. 调用父类即 HashMap 的 getNode 获取键值对,若为空则直接返回。
  2. 判断 accessOrder 是否为 true,若为 true 则说明需要保证 LinkedHashMap 的链表访问有序性,执行步骤 3。
  3. 调用 LinkedHashMap 重写的 afterNodeAccess 将当前元素添加到链表末尾。

remove 方法后置操作——afterNodeRemoval

final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
  Node<K,V>[] tab; Node<K,V> p; int n, index;
   if ((tab = table) != null && (n = tab.length) > 0 &&
       (p = tab[index = (n - 1) & hash]) != null) {
       Node<K,V> node = null, e; K k; V v;
       if (p.hash == hash &&
           ((k = p.key) == key || (key != null && key.equals(k))))
           node = p;
       else if ((e = p.next) != null) {
           if (p instanceof TreeNode)
               node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
           else {
               do {
                   if (e.hash == hash &&
                       ((k = e.key) == key ||
                        (key != null && key.equals(k)))) {
                       node = e;
                       break;
                   }
                   p = e;
               } while ((e = e.next) != null);
           }
       }
       if (node != null && (!matchValue || (v = node.value) == value ||
                            (value != null && value.equals(v)))) {
           if (node instanceof TreeNode)
               ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
           else if (node == p)
               tab[index] = node.next;
           else
               p.next = node.next;
           ++modCount;
           --size;
           afterNodeRemoval(node);
           return node;
       }
   }
   return null;
}

void afterNodeRemoval(Node<K,V> e) { // unlink

//获取当前节点p、以及e的前驱节点b和后继节点a
    LinkedHashMap.Entry<K,V> p =
        (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
//将p的前驱和后继指针都设置为null,使其和前驱、后继节点断开联系
    p.before = p.after = null;

//如果前驱节点为空,则说明当前节点p是链表首节点,让head指针指向后继节点a即可
    if (b == null)
        head = a;
    else
    //如果前驱节点b不为空,则让b直接指向后继节点a
        b.after = a;

//如果后继节点为空,则说明当前节点p在链表末端,所以直接让tail指针指向前驱节点a即可
    if (a == null)
        tail = b;
    else
    //反之后继节点的前驱指针直接指向前驱节点
        a.before = b;
}

从源码可以看出, afterNodeRemoval 方法的整体操作就是让当前节点 p 和前驱节点、后继节点断开联系,等待 gc 回收

put 方法后置操作——afterNodeInsertion

为了维护双向链表访问的有序性,它做了这样两件事:

  1. 重写 afterNodeAccess(上文提到过),如果当前被插入的 key 已存在与 map 中,因为 LinkedHashMap 的插入操作会将新节点追加至链表末尾,所以对于存在的 key 则调用 afterNodeAccess 将其放到链表末端。
  2. 重写了 HashMap 的 afterNodeInsertion 方法,当 removeEldestEntry 返回 true 时,会将链表首节点移除。
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)
        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;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

void afterNodeInsertion(boolean evict) { // possibly remove eldest
   LinkedHashMap.Entry<K,V> first;
    //如果evict为true且队首元素不为空以及removeEldestEntry返回true,则说明我们需要最老的元素(即在链表首部的元素)移除。
    if (evict && (first = head) != null && removeEldestEntry(first)) {
    	//获取链表首部的键值对的key
        K key = first.key;
        //调用removeNode将元素从HashMap的bucket中移除,并和LinkedHashMap的双向链表断开,等待gc回收
        removeNode(hash(key), key, null, false, true);
    }
}

LinkedHashMap 和 HashMap 遍历性能比较

 final class EntryIterator extends HashIterator
 implements Iterator < Map.Entry < K, V >> {
     public final Map.Entry < K,
     V > next() {
         return nextNode();
     }
 }

 //获取下一个Node
 final Node < K, V > nextNode() {
     Node < K, V > [] t;
     //获取下一个元素next
     Node < K, V > e = next;
     if (modCount != expectedModCount)
         throw new ConcurrentModificationException();
     if (e == null)
         throw new NoSuchElementException();
     //将next指向bucket中下一个不为空的Node
     if ((next = (current = e).next) == null && (t = table) != null) {
         do {} while (index < t.length && (next = t[index++]) == null);
     }
     return e;
 }

 final class LinkedEntryIterator extends LinkedHashIterator
 implements Iterator < Map.Entry < K, V >> {
     public final Map.Entry < K,
     V > next() {
         return nextNode();
     }
 }
 //获取下一个Node
 final LinkedHashMap.Entry < K, V > nextNode() {
     //获取下一个节点next
     LinkedHashMap.Entry < K, V > e = next;
     if (modCount != expectedModCount)
         throw new ConcurrentModificationException();
     if (e == null)
         throw new NoSuchElementException();
     //current 指针指向当前节点
     current = e;
     //next直接当前节点的after指针快速定位到下一个节点
     next = e.after;
     return e;
 }

LinkedHashMap 维护了一个双向链表来记录数据插入的顺序,因此在迭代遍历生成的迭代器的时候,是按照双向链表的路径进行遍历的。这一点相比于 HashMap 那种遍历整个 bucket 的方式来说,高效许多。这一点我们可以从两者的迭代器中得以印证,先来看看 HashMap 的迭代器,可以看到 HashMap 迭代键值对时会用到一个 nextNode 方法,该方法会返回 next 指向的下一个元素,并会从 next 开始遍历 bucket 找到下一个 bucket 中不为空的元素 Node。相比之下 LinkedHashMap 的迭代器则是直接使用通过 after 指针快速定位到当前节点的后继节点,简洁高效许多。

LinkedHashMap 需要维护双向链表的缘故,插入元素相较于 HashMap 会更耗时,但是有了双向链表明确的前后节点关系,迭代效率相对于前者高效了许多。

常见面试题

什么是 LinkedHashMap?

LinkedHashMap 是 Java 集合框架中 HashMap 的一个子类,它继承了 HashMap 的所有属性和方法,并且在 HashMap 的基础重写了 afterNodeRemoval、afterNodeInsertion、afterNodeAccess 方法。使之拥有顺序插入和访问有序的特性。

LinkedHashMap 如何按照插入顺序迭代元素?

LinkedHashMap 按照插入顺序迭代元素是它的默认行为。LinkedHashMap 内部维护了一个双向链表,用于记录元素的插入顺序。因此,当使用迭代器迭代元素时,元素的顺序与它们最初插入的顺序相同。

LinkedHashMap 如何按照访问顺序迭代元素?

LinkedHashMap 可以通过构造函数中的 accessOrder 参数指定按照访问顺序迭代元素。当 accessOrder 为 true 时,每次访问一个元素时,该元素会被移动到链表的末尾,因此下次访问该元素时,它就会成为链表中的最后一个元素,从而实现按照访问顺序迭代元素。

LinkedHashMap 如何实现 LRU 缓存?

将 accessOrder 设置为 true 并重写 removeEldestEntry 方法当链表大小超过容量时返回 true,使得每次访问一个元素时,该元素会被移动到链表的末尾。一旦插入操作让 removeEldestEntry 返回 true 时,视为缓存已满,LinkedHashMap 就会将链表首元素移除,由此我们就能实现一个 LRU 缓存。

LinkedHashMap 和 HashMap 有什么区别?

LinkedHashMap 和 HashMap 都是 Java 集合框架中的 Map 接口的实现类。它们的最大区别在于迭代元素的顺序。HashMap 迭代元素的顺序是不确定的,而 LinkedHashMap 提供了按照插入顺序或访问顺序迭代元素的功能。此外,LinkedHashMap 内部维护了一个双向链表,用于记录元素的插入顺序或访问顺序,而 HashMap 则没有这个链表。因此,LinkedHashMap 的插入性能可能会比 HashMap 略低,但它提供了更多的功能并且迭代效率相较于 HashMap 更加高效。

作者声明

如有问题,欢迎指正!

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

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

相关文章

北峰北斗短报文在应急通信的应用

随着社会的发展&#xff0c;自然灾害、交通事故等突发事件的频繁发生&#xff0c;让人们知道通信设备的可靠性尤为重要。北斗短报文应急通信作为一种新型的通信方式&#xff0c;具有较高的可靠性和应用价值。尤其是在灾区移动通讯中断&#xff0c;电力中断或移动通信无法覆盖北…

React 之 Hooks解析

一、概念 1. class组件的优势 class组件可以定义自己的state&#xff0c;用来保存组件自己内部的状态 函数式组件不可以&#xff0c;因为函数每次调用都会产生新的临时变量class组件有自己的生命周期&#xff0c;我们可以在对应的生命周期中完成自己的逻辑&#xff0c;比如在…

使用Postman拦截浏览器请求

项目上线之后&#xff0c;难免会有BUG。在出现问题的时候&#xff0c;我们可能需要获取前端页面发送请求的数据&#xff0c;然后在测试环境发送相同的数据将问题复现。手动构建数据是挺麻烦的一件事&#xff0c;所以我们可以借助Postman在浏览器上的插件帮助拦截请求&#xff0…

2023最新PDF阅读器评测

评测说明 本人程序员&#xff0c;平时阅读为主。以下为主观实际体验感受为主。 软件选择 以无广、可免费使用为基本要求。Adobe Reader 自不必说。 体验软件 SumatraPDF 特点&#xff1a;简洁。开源免费的小个子软件&#xff0c;当前最新安装包只有7M&#xff0c;启动速度很…

【初阶算法4】——归并排序的详解,及其归并排序的扩展

目录 前言 学习目标&#xff1a; 学习内容&#xff1a; 一、介绍归并排序 1.1 归并排序的思路 1.2 归并排序的代码 1.2.1 mergesort函数部分 1.2.2 process函数部分 1.2.3 merge函数部分 二、AC两道经典的OJ题目 题目一&#xff1a;逆序对问题 题目二&#xff1…

笔记本选购指南

大学生笔记本电脑选购指南 文章目录 笔记本分类指标排行 了解自身需求理工科文科艺术总结 参考指标品牌CPU显卡屏幕其他 购买渠道推荐游戏本Redmi G 锐龙版联想G5000惠普光影精灵9天选4锐龙版联想R7000P暗影精灵9联想拯救者R9000P 全能本华硕无畏PRO15联想小新Pro14 2023 轻薄本…

react ant ice3 实现点击一级菜单自动打开它下面最深的第一个子菜单

1.问题 默认的如果没有你的菜单结构是这样的&#xff1a; [{children: [{name: "通用配置"parentId: "1744857774620672"path: "basic"}],name: "系统管理"parentId: "-1"path: "system"} ]可以看到每层菜单的p…

期权投资的优势有哪些方面?

随着金融市场的不断演变&#xff0c;越来越多的金融衍生品出现在人们的视线中&#xff0c;特别是上证50ETF期权可以做空T0的交易模式吸引了越来越多的朋友&#xff0c;那么期权投资的优势有哪些方面&#xff1f; 期权是投资市场中一个非常重要的投资方式&#xff0c;期权投资能…

SOLIDWORKS装配体如何使用全局变量

客户痛点&#xff1a;随着人力资源价格的增长&#xff0c;设计的时间需要减少时间&#xff0c;提高设计效率。 数据问题&#xff1a;以前单个数据都需要建立单独的数据结构&#xff0c;装配体的模型都要重新建立。 需要解决的问题&#xff1a;能够快速地完成3D模型及装配体的…

TensorFlow 03(Keras)

一、tf.keras tf.keras是TensorFlow 2.0的高阶API接口&#xff0c;为TensorFlow的代码提供了新的风格和设计模式&#xff0c;大大提升了TF代码的简洁性和复用性&#xff0c;官方也推荐使用tf.keras来进行模型设计和开发。 1.1 tf.keras中常用模块 如下表所示: 1.2 常用方法 …

机器学习——协同过滤算法(CF)

机器学习——协同过滤算法&#xff08;CF&#xff09; 文章目录 前言一、基于用户的协同过滤1.1. 原理1.2. 算法步骤1.3. 代码实现 二、基于物品的协同过滤2.1. 原理2.2. 算法步骤2.3. 代码实现 三、比较与总结四、实例解析总结 前言 协同过滤算法是一种常用的推荐系统算法&am…

清理 Ubuntu 系统的 4 个简单步骤

清理 Ubuntu 系统的 4 个简单步骤 现在&#xff0c;试试看这 4 个简单的步骤&#xff0c;来清理你的 Ubuntu 系统吧。 这份精简指南将告诉你如何清理 Ubuntu 系统以及如何释放一些磁盘空间。 如果你的 Ubuntu 系统已经运行了至少一年&#xff0c;尽管系统是最新的&#xff0c;…

2003-2022年黄河流域TCI、VCI、VHI、TVDI逐年1km分辨率数据集

摘要 黄河流域大部分属于干旱、半干旱气候,先天水资源条件不足,是中国各大流域中受干旱影响最为严重的流域。随着全球环境和气候变化,黄河流域的干旱愈加频繁,对黄河流域的干旱监测研究已经成为当下的热点。本数据集基于MODIS植被和地表温度产品,通过对逐年数据进行去云、…

Mendix使用Upload image新增修改账户头像

学习Mendix中级文档&#xff0c;其中有个管理我的账号功能&#xff0c;确保账号主任可以修改其头像&#xff0c;接下来记录如何实现账户头像的上传和修改。根据文档的步骤实现功能&#xff5e;&#xff5e; 新建GeneralExtentions模块&#xff0c;给GeneralExtentions添加两个模…

MapTR v2文章研读

MapTR v2论文来了&#xff0c;本文仅介绍v2相较于v1有什么改进之处&#xff0c;如果想了解v1版本的论文细节&#xff0c;可见链接。 相较于maptr&#xff0c;maptr v2改进之处&#xff1a; 在分层query机制中引进解耦自注意力机制&#xff0c;有效降低了内存消耗&#xff1b;…

Spring中如何解决循环依赖问题

一、什么是循环依赖 循环依赖也叫循环引用&#xff0c;是指bean之间形成相互依赖的关系&#xff0c;由此&#xff0c;bean对象在属性注入时便会产生循环。这种循环依赖会导致编译器无法编译代码&#xff0c;从而无法运行程序。为了避免循环依赖&#xff0c;我们在开发过程中需…

视频号视频下载工具有那些?我们怎么下载视频号里面的视频

本篇文章给大家谈谈视频号视频下载工具&#xff0c;以及视频号视频如何下载?对应的知识点&#xff0c;希望对各位有所帮助。 视频号里面的视频可以下载吗&#xff1f; 视频号官方首先是不提供下载功能的&#xff0c;但是很多第三方可以提供视频号的视频下载功能。 早期版本视…

【力扣每日一题】2023.9.12 课程表Ⅳ

目录 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 代码&#xff1a; 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 今天是课程表系列题目的最后一题&#xff0c;因为我在题库里找不到课程表5了&#xff0c;所以今天的每日一题就是最后一个课程表了。 题…

小节5:Python列表list常用操作

1、对列表的基本认知&#xff1a; 列表list&#xff0c;是可变类型。比如&#xff0c;append()函数会直接改变列表本身&#xff0c;往列表里卖弄添加元素。所以&#xff0c;list_a list_a.append(123)就是错误的。如果想删除列表中的元素&#xff0c;可以用remove()函数&…

基于微信小程序的宠物寄养平台,附源码、数据库

1. 简介 本文正是基于微信小程序开发平台&#xff0c;针对宠物寄养的需求,本文设计出一个包含寄养家庭分类、寄养服务管理、宠物档案、交流论坛的微信小程序,以此帮助宠物寄养的实现,促进宠物寄养工作的进展。 2 开发技术 微信小程序的运行环境分为渲染层和逻辑层&#xff0…