LinkedHashMap源码解析

news2024/12/23 19:22:17

LinkedHashMap源码解析

简介

LinkedHashMap 继承于 HashMap,其内部的 Entry 多了两个前驱后继指针,内部额外维护了一个双向链表,能保证元素按插入的顺序访问,也能以访问顺序访问,可以用来实现 LRU 缓存策略

LinkedHashMap 实现了 Map 接口,即允许放入 keynull 的元素,也允许插入 valuenull 的元素。

LinkedHashMap 可以看成是 LinkedList + HashMap,从名字上可以看出该容器是 LinkedList 和 HashMap 的混合体,也就是说它同时满足 HashMap 和 LinkedList 的某些特性。可将 LinkedHashMap 看作采用 LinkedList 增强的 HashMap。

继承体系

6f839a253fa646e099426b81be61300d

LinkedHashMap 继承与 HashMap,核心的增删改查基本还是 HashMap 中的方法,只是 LinkedHashMap 实现了几个钩子函数,可以在添加删除等最后一步调用 LinkedHashMap 实现的钩子函数进行额外的操作,下面会详细讲解。

存储结构

LinkedHashMap-structure

我们知道 HashMap 使用 数组 + 单链表 + 红黑树 的存储结构,那 LinkedHashMap 是怎么存储的呢?

通过上面的继承体系,我们知道它继承了 HashMap,所以它的内部也有这三种结构,但是它还额外添加了一种 “双向链表” 的结构存储所有元素的顺序。

添加删除元素的时候需要同时维护在 HashMap 中的存储,也要维护在 LinkedList 中的存储,所以性能上来说会比 HashMap 稍慢。

简单使用

public class Main {
    public static void main(String[] args) {
        HashMap<String, String> hashMap = new HashMap<>();
        hashMap.put("name", "张三");
        hashMap.put("age", "13");
        hashMap.put("gender", "男");
        System.out.println(hashMap);

        LinkedHashMap<String, String> linkedHashMap = new LinkedHashMap<>();
        linkedHashMap.put("name", "张三");
        linkedHashMap.put("age", "13");
        linkedHashMap.put("gender", "男");
        System.out.println(linkedHashMap);
    }
}

/*
 * 控制台打印
 * {gender=男, name=张三, age=13} // HashMap 无序
 * {name=张三, age=13, gender=男} // LinkedHashMap(默认)按照元素添加的顺序遍历
 */

源码解析

内部类Entry

  	/*
  	 * LinkedHashMap中的Entry节点,继承了HashMap中的Node。
   	 */
	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);
        }
    }

	/*
	 * HashMap中的Node节点
	 */
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
    }

存储节点 Entry,继承自 HashMap 的 Node 类,next 用于单链表存储于桶中,before 和 after 用于双向链表存储所有元素。

属性

    /*
     * 双向链表头节点,旧数据存在头节点。
     */
    transient LinkedHashMap.Entry<K,V> head;

    /*
     * 双向链表尾节点,新数据存在尾节点。
     */
    transient LinkedHashMap.Entry<K,V> tail;

    /*
     * 是否需要按访问顺序排序
	 * true:双向链表按照元素的访问顺序排序(LRU)。
	 * false:按照元素添加顺序排序。
	 */
    final boolean accessOrder;

构造方法

  	/*
  	 * 传入初始容量和加载因子。
  	 */
	public LinkedHashMap(int initialCapacity, float loadFactor) {
        // 调用HashMap的构造器
        super(initialCapacity, loadFactor);
        // accessOrder置为false,表示双向链表按照元素的添加顺序构建。
        accessOrder = false;
    }

    public LinkedHashMap(int initialCapacity) {
        super(initialCapacity);
        accessOrder = false;
    }

    public LinkedHashMap() {
        super();
        accessOrder = false;
    }

    public LinkedHashMap(Map<? extends K, ? extends V> m) {
        super();
        accessOrder = false;
        putMapEntries(m, false);
    }
	
	/*
	 * 上面的4个构造器基本都差不多,都直接将accessOrder置为了false。
	 * 而此构造器可以自定义accessOrder。
	 */
    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        // 为初始容量和加载因子赋值
        super(initialCapacity, loadFactor);
        // 为accessOrder赋值
        this.accessOrder = accessOrder;
    }

前四个构造方法 accessOrder 都等于 false,说明双向链表是按插入顺序存储元素。

最后一个构造方法 accessOrder 从构造方法参数传入,如果传入 true,则就实现了按访问顺序存储元素,这也是实现 LRU 缓存策略的关键

扩展:LRU 缓存机制

在解析核心方法之前,我们先来了解一下什么是 LRU(Least Recently Used)最近最少使用算法,是操作系统中一种常用的页面置换算法,有兴趣的同学们可以去做一下这道题:LeetCode146.LRU缓存,这道题考察的就是数据结构封装的能力,本题使用 HashMap + 手动构建双向链表 实现了一个简单的 LRU 算法,题解参考。做到了题目中要求的 get() 和 put() 方法的平均时间复杂度为 O ( 1 ) O(1) O(1)

其实这道题我们可以直接使用 LinkedHashMap 来实现,(ps:毕竟是算法题,还是得自己实现,不要直接使用LinkedHashMap来实现,不然面试时候直接gg)

  • 使用 LinkedHashMap 就能实现的原理就是 HashMap 中留给 LinkedHashMap 实现的钩子函数。
    • 当进行 put 操作后,如果当前的元素个数大于了规定容量 (LRU场景下有最大容量) ,那么就调用对应的钩子函数将最不经常使用的节点从链表和 map 中删除(这里是头结点)。
    • 当访问了某个节点后 (一般是修改和查询) 调用对应的钩子函数将此节点插入到链表的结尾,表示当前节点是当前最热门的节点。

当对 LRU 有了一定的认识后,下面的几个钩子方法我们就能很简单的搞定了!

HashMap#put()

LinkedHashMap 没有重写 put() 方法,即调用的还是 HashMap#put() 方法,只是重写了一些方法。

所以我们我们大致看一下 HashMap#put() 方法,详细讲解参考HashMap源码解析。

	public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
	}
				||
				\/
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
       		
        	// 省略了四种情况的添加操作。
        	// 这里是插入操作,调用了newNode()方法。

        	// 这里是替换操作
            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;
    }

我们再来看一下 LinkedHashMap 重写的 newNode 方法:

  • 就是先构建一个 LinkedHashMap 内部的 Entry 节点,然后将节点插入到双向链表的末尾。
    Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    	// 创建的不是HashMap中的Node,而是LinkedHashMap中的Entry节点
        LinkedHashMap.Entry<K,V> p =
            new LinkedHashMap.Entry<K,V>(hash, key, value, e);
        // 将p插入到双向链表的尾部
        linkNodeLast(p);
        return p;
    }
				||
				\/
	// 将节点p插入到双向链表的尾部。
    private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
        // tail是双向链表尾结点,用last存一下
        LinkedHashMap.Entry<K,V> last = tail;
        // tail指向插入节点
        tail = p;
        // last = null 说明双向链表中没有节点
        if (last == null)
            // 将head(头节点)指向p
            head = p;
        // last != null 说明双向链表不为null
        else {
            // 将p的前驱指向原尾结点
            p.before = last;
            // 原尾结点的后继指向p,完成插入。
            last.after = p;
        }
    }

afterNodeInsertion(boolean evict)

  • 在节点插入之后做些什么,在 HashMap 中的 putVal() 方法中被调用,HashMap 中这个方法的实现为空,就是留给 LinkedHashMap 实现的。
  • 即在插入一个节点后是否需要将头结点从链表和 Map 中移除(在实现LRU Cache时,如果当前元素个数大于规定的容量,需要将最不常使用的节点删除,此时最不常使用的节点就是头节点)。
   /*
    * 表示在插入节点后该做什么。
    */
	void afterNodeInsertion(boolean evict) { 
        LinkedHashMap.Entry<K,V> first;
        /*
         * evict固定为true。
         * removeEldestEntry默认为false,即不删除头节点,留给我们重写。
         */
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            // 头结点
            K key = first.key;
            /*
             * 调用HashMap的removeNode()方法,将节点从哈希表中删除
             * 此方法中也有一个钩子方法afterNodeRemoval,即将此节点从链表中删除。
             */
            removeNode(hash(key), key, null, false, true);
        }
    }

	/*
	 * 此方法是LinkedHashMap的方法,默认是返回false,留给我们重写,后面实现LRU的核心就是此方法。
	 */
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }

evict,驱逐的意思。

  1. 如果 evict 为 true,且头节点不为空,且确定移除最老的元素,那么就调用 HashMap.removeNode() 把头节点移除(这里的头节点是双向链表的头节点,而不是某个桶中的第一个元素);
  2. HashMap.removeNode() 从 HashMap 中把这个节点移除之后,会调用 afterNodeRemoval() 方法;
  3. afterNodeRemoval() 方法在 LinkedHashMap 中也有实现,用来在移除元素后修改双向链表,见下文;
  4. 默认 removeEldestEntry() 方法返回 false,也就是不删除元素。

afterNodeAccess(Node<K,V> e)

  • 在节点访问之后被调用,主要在 put() 已经存在的元素或 get() 时被调用,如果 accessOrder 为 true,调用这个方法把访问到的节点移动到双向链表的末尾。
    void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last; // 链表尾结点
        /*
         * 条件1:accessOrder为true,表示按照访问顺序排序
         * 条件2:e这个节点不是尾结点(如果是尾结点的话无需操作)
         */
        if (accessOrder && (last = tail) != e) {
            /*
             * p:当前需要移动的节点
             * b:p的前驱
             * a:p的后继
             */
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            // 将p的后继置为null
            p.after = null;
// ---------将p节点从链表中删除      
            // p的前驱为null,说明p是头结点
            if (b == null)
                // 将头节点变为p的后继
                head = a;
            // p不是头节点
            else
                // 将p的前驱节点的后继指向p的后继,
                b.after = a;
            
            // a不为null,说明p有后继节点
            if (a != null)
                //将p的后继节点的前驱指向p的前驱(完成删除p节点)
                a.before = b;
            else
                last = b;
// ---------将p节点插入到末尾
            if (last == null)
                head = p;
            else {
                // p的前驱指向尾结点
                p.before = last;
                // 尾结点的后继指向p
                last.after = p;
            }
            // 最终的尾结点就是p
            tail = p;
            /*
             * 注意这里modCount会变化(faild-fast机制),
             * 因为当你在遍历LinkedHashMap时,同时有线程访问数据,会造成链表结构发生变化,直接抛异常。
             */
            ++modCount;
        }
    }

(1)如果 accessOrder 为 true,并且访问的节点不是尾节点;

(2)从双向链表中移除访问的节点;

(3)把访问的节点加到双向链表的末尾;(末尾为最新访问的元素)

afterNodeRemoval(Node<K,V> e)

  • 在节点被删除之后调用的方法,在 HashMap 中的 removeNode() 方法中被调用。
    /*
     * 将节点从双向链表中删除。
     */
	void afterNodeRemoval(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;
        // 把节点p从双向链表中删除。
        if (b == null)
            head = a;
        else
            b.after = a;
        if (a == null)
            tail = b;
        else
            a.before = b;
    }

get(Object key)

  • 查找元素
    public V get(Object key) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
    }

如果查找到了元素,且 accessOrder 为 true,则调用 afterNodeAccess() 方法把访问的节点移到双向链表的末尾。

LinkedHashMap 实现 LRU

LinkedHashMap 如何实现 LRU 缓存淘汰策略呢?

关于 LRU,Least Recently Used,最近最少使用算法,也就是优先淘汰最近最少使用的元素。在上面我们已经介绍过了。

经过上方的代码分析,我们发现只需要达成两个条件即可:

  • accessOrder 赋值为 true
  • 重写 removeEldestEntry() 方法

代码实现

import java.util.LinkedHashMap;
import java.util.Map;

public class LRU<K, V> extends LinkedHashMap<K, V> {

    private int maxSize;

    public LRU(int size, float loadFactory) {
        /*
         * 调用LinkedHashMap中唯一的一个可以为accessOrder赋值的构造器
         * 为accessOrder赋值为true,表示链表的顺序为LRU。
         */
        super(size, loadFactory, true);
        // 设定缓存的最大容量
        this.maxSize = size;
    }

    /*
     * 重写removeEldestEntry规定当缓存中元素满时进行删除最不常使用的元素。
     */
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        // 当元素个数大于了缓存的容量,就移除元素
        return size() > this.maxSize;
    }
}

测试

    public static void main(String[] args) {
        LRU<String, String> cache = new LRU<String, String>(10, 0.75f);
        cache.put("name", "李四"); // name
        cache.put("age", "13");   // name -> age
        cache.put("gender", "男"); // name -> age -> gender
        cache.put("age", "15");   // 这里age使用了 name -> gender -> age
        cache.put("class", "19407"); // name -> gender -> age -> class
        cache.put("name", "张三"); // 这里name使用了 gender -> age -> class -> name

        for (Map.Entry<String, String> e : cache.entrySet()){
        	System.out.println(e.getKey() + "  " + e.getValue());
        }
    }

输出

gender  男
age  15
class  19407
name  张三

如果我们将 LRU 的 size 属性设置为 3,则输出:

age  15
class  19407
name  张三

头结点 gender 被淘汰掉了。

我们使用 LinkedHashMap 实现了 LRU 缓存淘汰策略!

总结

(1)LinkedHashMap 继承自 HashMap,具有 HashMap 的所有特性;

(2)LinkedHashMap 内部维护了一个双向链表存储所有的元素;

(3)如果 accessOrder 为 false,则可以按插入元素的顺序遍历元素;

(4)如果 accessOrder 为 true,则可以按访问元素的顺序遍历元素(LRU);

(5)LinkedHashMap 的实现非常精妙,很多方法都是在 HashMap 中留的钩子(Hook),直接实现这些 Hook 就可以实现对应的功能了,并不需要再重写 put() 等方法;

(6)默认的 LinkedHashMap 并不会移除旧元素,如果需要移除旧元素,则需要重写 removeEldestEntry() 方法设定移除策略;

(7)LinkedHashMap 可以用来实现 LRU 缓存淘汰策略(accessOrder 赋值为 true,重写 removeEldestEntry() 方法);




参考文章

  • 彤哥读源码_死磕 java集合之LinkedHashMap源码分析
  • shstart7_LinkedHashMap源码解析

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

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

相关文章

linux造so及调用

1.so的代码 2.gcc -O -c -fPIC -o testso.o so.c 3.gcc -shared -o testso.so testso.o 4.so可以看到了。 5.拷贝到系统内&#xff0c;然后同时ldconfig更新下。 6.调用的c代码 7.编译&#xff0c;连接。 8.可以看到执行的结果了。 ######################################…

分布式光伏运维平台具体有哪些功能?

安科瑞 华楠 综合看板 1、显示所有光伏电站的数量&#xff0c;装机容量&#xff0c;实时发电功率。 2、累计日、月、年发电量及发电收益。 3、累计社会效益。 4、柱状图展示月发电量 电站状态 1、电站状态展示当前光伏电站发电功率&#xff0c;补贴电价&#xff0c;峰值功…

2022游戏安全行业峰会举办,生态共建护航游戏产业

游戏的外挂、黑产、盗版等问题&#xff0c;一直是运营过程中面临的重要难题。这些安全问题&#xff0c;轻则给游戏的收入和口碑带来损伤&#xff0c;重则可以摧毁一款游戏。因此&#xff0c;近年来越来越多的游戏厂商不断加大对游戏安全的投入&#xff0c;为游戏建造铜墙铁壁。…

说说Vue响应式系统中的Watcher和Dep的关系-面试进阶

引言 在这里我先提出两个问题&#xff08;文章末尾会进行解答&#xff09;&#xff1a; 在Vue的数据响应系统中&#xff0c;Dep和Watcher各自分担什么任务&#xff1f;Vue的数据响应系统的核心是Object.defineproperty一定是最好的吗&#xff1f;有什么弊端和漏洞吗&#xff…

【毕业设计_课程设计】基于Android的二维码扫描库

文章目录0 项目说明1 实现功能2 使用方式3 项目工程0 项目说明 基于Android的二维码扫描库 提示&#xff1a;适合用于课程设计或毕业设计&#xff0c;工作量达标&#xff0c;源码开放 1 实现功能 可打开默认二维码扫描页面支持对图片Bitmap的扫描功能支持对UI的定制化操作支…

Problem C: 算法9-9~9-12:平衡二叉树的基本操作

Problem Description 平衡二叉树又称AVL树&#xff0c;它是一种具有平衡因子的特殊二叉排序树。平衡二叉树或者是一棵空树&#xff0c;或者是具有以下几条性质的二叉树&#xff1a; 1. 若它的左子树不空&#xff0c;则左子树上所有结点的值均小于它的根节点的值&#xf…

JavaSE笔记——内部类

文章目录前言一、创建内部类二、链接外部类三、使用.this 和.new四、内部类与向上转型五、内部类方法和作用域六、匿名内部类七、嵌套类1. 接口内部的类2. 从多层嵌套类中访问外部类的成员八、继承内部类九、局部内部类总结前言 一个定义在另一个类中的类&#xff0c;叫作内部…

AUTO-CUT安装。

auto cut 是李沐老师分享的视频剪辑工具wisper。 这我非常需要啊。 所以看看怎么安装。 先下载客户端。和ffmpeg 链接&#xff1a;https://pan.baidu.com/s/1EZomUtV8Y_es8crR6-r3GQ 提取码&#xff1a;lgky 解压安装autocut客户端。 还需要ffmpeg和环境。 环境配置是自动…

事业编上岸浙大mpa的个人经验总结

先来介绍下我的个人情况吧&#xff0c;我是2022级浙大MPA项目的一名新生&#xff0c;也是一名中学老师&#xff0c;定居在杭州&#xff0c;毕业于湖州师范学院&#xff0c;在学校主要是负责教研这一块的工作&#xff0c;因为工作需要的原因&#xff0c;在综合了解了几个项目后&…

测试工具Hercules

下载地址&#xff1a;Hercules SETUP utility | HW-group.com Hercules SETUP实用程序是有用的串行端口终端&#xff08;RS-485或RS-232终端&#xff09;、UDP/IP终端和TCP/IP客户端服务器终端。它只为硬件组内部使用而创建&#xff0c;但今天它在一个实用程序中包含了许多功能…

手把手刷算法项目fucking-algorithm,干翻算法

今天给大家分享一个开源项目&#xff0c;在 GitHub 排行榜上今天特别火&#xff0c;都爬到了日排行榜的第二名。 大家想知道&#xff0c;面试互联网大厂&#xff0c;必面的是什么吗&#xff1f;当然是算法。作为程序员&#xff0c;互联网大厂的面试&#xff0c;算法是最重要的…

用javascript分类刷leetcode10.递归分治(图文视频讲解)

递归三要素 递归函数以及参数递归终止条件递归单层搜索逻辑 递归伪代码模版&#xff1a; function recursion(level, param1, param2, ...) {//递归终止条件if (level > MAX_LEVEL) {// output resultreturn;}//处理当前层process_data(level, data, ...);//进入下一层re…

10_缓存-2_二级缓存

二级缓存是以namespace为标记的缓存&#xff0c;可以是由一个SqlSessionFactory创建的SqlSession之间共享缓存数据。默认并不开启。下面的代码中创建了两个SqlSession&#xff0c;执行相同的SQL语句&#xff0c;尝试让第二个SqlSession使用第一个SqlSession查询后缓存的数据。要…

CTF Misc(1)图片隐写基础以及原理,覆盖了大部分题型

前言 在ctf比赛中&#xff0c;misc方向是必考的一个方向&#xff0c;其中&#xff0c;图片隐写也是最常见的题目类型&#xff0c;在本篇文章中&#xff0c;将教授以下内容 1.各种图片文件的头数据以及判断是什么类型的图片 2.png图片隐写 3.jpg图片隐写 4.gif图片隐写 5.bmp图…

Android---RecyclerView实现吸顶效果

目录 一、ItemDecoration 二、实现RecyclerView吸顶效果 1、实现一个简单的RecyclerView。 2、通过ItemDecoration画分割线 3、画出每个分组的组名 4、实现吸顶效果 完整demo 一、ItemDecoration ItemDecoration 允许应用给具体的 View 添加具体的图画或者 layout 的偏移…

论文投稿指南——中文核心期刊推荐(物理学)

【前言】 &#x1f680; 想发论文怎么办&#xff1f;手把手教你论文如何投稿&#xff01;那么&#xff0c;首先要搞懂投稿目标——论文期刊 &#x1f384; 在期刊论文的分布中&#xff0c;存在一种普遍现象&#xff1a;即对于某一特定的学科或专业来说&#xff0c;少数期刊所含…

[附源码]计算机毕业设计Python的高校课程知识库(程序+源码+LW文档)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程 项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等…

如何做好源代码防泄密

​ 一、前言 • 各类嵌入式研发及平台软件研发行业&#xff0c;都有自己的核心数据以及核心文档&#xff0c;用户数据等敏感信息&#xff0c;这些信息数据有以下共性&#xff1a; –属于核心机密资料&#xff0c;万一泄密会给造成恶劣影响 –核心数据类型多&#xff0c;有…

验证码是自动化的天敌?看看大神是怎么解决的

01 验证码 1、什么是验证码&#xff1a; 指一种随机生成的信息&#xff08;数字、字母、汉字、图片、算术题&#xff09;等为了防止恶意的请求行为&#xff0c;增加应用的安全性 自动化过程中也是需要进行注册或者登陆的操作&#xff0c;所以需要处理验证 2、验证码处理方式…

《第一堂棒球课》:MLB棒球创造营·棒球名人堂

铃木一朗&#xff0c;1973年10月22日出生于西春日井郡丰山町&#xff08;日本&#xff09;&#xff0c;日本职业棒球运动员&#xff0c;效力于美国职棒大联盟西雅图水手队。 1991年被欧力士蓝浪以第四指名选中&#xff0c;1994年以片假名&#xff08;Ichiro&#xff09;在一军…