面试必备:LinkedHashMap源码解析(JDK8)

2024-06-11 05:48

本文主要是介绍面试必备:LinkedHashMap源码解析(JDK8),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

概括的说,LinkedHashMap 是一个关联数组、哈希表,它是线程不安全的,允许key为null,value为null。 
它继承自HashMap,实现了Map<K,V>接口。其内部还维护了一个双向链表,在每次插入数据,或者访问、修改数据时,会增加节点、或调整链表的节点顺序。以决定迭代时输出的顺序。

默认情况,遍历时的顺序是按照插入节点的顺序。这也是其与HashMap最大的区别。 
也可以在构造时传入accessOrder参数,使得其遍历顺序按照访问的顺序输出。

因继承自HashMap,所以HashMap上文分析的特点,除了输出无序,其他LinkedHashMap都有,比如扩容的策略,哈希桶长度一定是2的N次方等等。 
LinkedHashMap在实现时,就是重写override了几个方法。以满足其输出序列有序的需求。

示例代码:

根据这段实例代码,先从现象看一下LinkedHashMap的特征: 
在每次插入数据,或者访问、修改数据时,会增加节点、或调整链表的节点顺序。以决定迭代时输出的顺序。

        Map<String, String> map = new LinkedHashMap<>();map.put("1", "a");map.put("2", "b");map.put("3", "c");map.put("4", "d");Iterator<Map.Entry<String, String>> iterator = map.entrySet().iterator();while (iterator.hasNext()) {System.out.println(iterator.next());}System.out.println("以下是accessOrder=true的情况:");map = new LinkedHashMap<String, String>(10, 0.75f, true);map.put("1", "a");map.put("2", "b");map.put("3", "c");map.put("4", "d");map.get("2");//2移动到了内部的链表末尾map.get("4");//4调整至末尾map.put("3", "e");//3调整至末尾map.put(null, null);//插入两个新的节点 nullmap.put("5", null);//5iterator = map.entrySet().iterator();while (iterator.hasNext()) {System.out.println(iterator.next());}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

输出:

1=a
2=b
3=c
4=d
以下是accessOrder=true的情况:
1=a
2=b
4=d
3=e
null=null
5=null
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

3 节点

LinkedHashMap的节点Entry<K,V>继承自HashMap.Node<K,V>,在其基础上扩展了一下。改成了一个双向链表

    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);}}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

同时类里有两个成员变量head tail,分别指向内部双向链表的表头、表尾。

    //双向链表的头结点transient LinkedHashMap.Entry<K,V> head;//双向链表的尾节点transient LinkedHashMap.Entry<K,V> tail;
  • 1
  • 2
  • 3
  • 4
  • 5

4 构造函数

    //默认是false,则迭代时输出的顺序是插入节点的顺序。若为true,则输出的顺序是按照访问节点的顺序。//为true时,可以在这基础之上构建一个LruCachfinal boolean accessOrder;public LinkedHashMap() {super();accessOrder = false;}//指定初始化时的容量,public LinkedHashMap(int initialCapacity) {super(initialCapacity);accessOrder = false;}//指定初始化时的容量,和扩容的加载因子public LinkedHashMap(int initialCapacity, float loadFactor) {super(initialCapacity, loadFactor);accessOrder = false;}//指定初始化时的容量,和扩容的加载因子,以及迭代输出节点的顺序public LinkedHashMap(int initialCapacity,float loadFactor,boolean accessOrder) {super(initialCapacity, loadFactor);this.accessOrder = accessOrder;}//利用另一个Map 来构建,public LinkedHashMap(Map<? extends K, ? extends V> m) {super();accessOrder = false;//该方法上文分析过,批量插入一个map中的所有数据到 本集合中。putMapEntries(m, false);}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

小结: 
构造函数和HashMap相比,就是增加了一个accessOrder参数。用于控制迭代时的节点顺序。

5 增

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

LinkedHashMap重写了newNode(),在每次构建新节点时,通过linkNodeLast(p);新节点链接在内部双向链表的尾部

    //在构建新节点时,构建的是`LinkedHashMap.Entry` 不再是`Node`.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;}//将新增的节点,连接在链表的尾部private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {LinkedHashMap.Entry<K,V> last = tail;tail = p;//集合之前是空的if (last == null)head = p;else {//将新节点连接在链表的尾部p.before = last;last.after = p;}}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

以及HashMap专门预留给LinkedHashMapafterNodeAccess() afterNodeInsertion() afterNodeRemoval() 方法。

    // Callbacks to allow LinkedHashMap post-actionsvoid afterNodeAccess(Node<K,V> p) { }//访问元素之后调用,accessOrder为true的情况下,将访问的元素移到链表末尾,更新双向链表
void afterNodeInsertion(boolean evict) { }//新增元素之后调用,根据evict判断是否需要删除最老插入的节点void afterNodeRemoval(Node<K,V> p) { }//移除元素之后调用,更新双向链表
  • 1
  • 2
  • 3
  • 4
    //回调函数,新节点插入之后回调 , 根据evict 和   判断是否需要删除最老插入的节点。如果实现LruCache会用到这个方法。void afterNodeInsertion(boolean evict) { // possibly remove eldestLinkedHashMap.Entry<K,V> first;//LinkedHashMap 默认返回false 则不删除节点if (evict && (first = head) != null && removeEldestEntry(first)) {K key = first.key;removeNode(hash(key), key, null, false, true);}}//LinkedHashMap 默认返回false 则不删除节点。 返回true 代表要删除最早的节点。通常构建一个LruCache会在达到Cache的上限是返回trueprotected boolean removeEldestEntry(Map.Entry<K,V> eldest) {return false;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

void afterNodeInsertion(boolean evict)以及boolean removeEldestEntry(Map.Entry<K,V> eldest)是构建LruCache需要的回调,在LinkedHashMap里可以忽略它们。

6 删

LinkedHashMap也没有重写remove()方法,因为它的删除逻辑和HashMap并无区别。 
但它重写了afterNodeRemoval()这个回调方法。该方法会在Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable)
方法中回调,removeNode()会在所有涉及到删除节点的方法中被调用,上文分析过,是删除节点操作的真正执行者。

    //在删除节点e时,同步将e从双向链表上删除void afterNodeRemoval(Node<K,V> e) { // unlinkLinkedHashMap.Entry<K,V> p =(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;//待删除节点 p 的前置后置节点都置空p.before = p.after = null;//如果前置节点是null,则现在的头结点应该是后置节点aif (b == null)head = a;else//否则将前置节点b的后置节点指向ab.after = a;//同理如果后置节点时null ,则尾节点应是bif (a == null)tail = b;else//否则更新后置节点a的前置节点为ba.before = b;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

7 查

LinkedHashMap重写了get()和getOrDefault()方法:

    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;}public V getOrDefault(Object key, V defaultValue) {Node<K,V> e;if ((e = getNode(hash(key), key)) == null)return defaultValue;if (accessOrder)afterNodeAccess(e);return e.value;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

对比HashMap中的实现,LinkedHashMap只是增加了在成员变量(构造函数时赋值)accessOrder为true的情况下,要去回调void afterNodeAccess(Node<K,V> e)函数。

    public V get(Object key) {Node<K,V> e;return (e = getNode(hash(key), key)) == null ? null : e.value;}
  • 1
  • 2
  • 3
  • 4

afterNodeAccess()函数中,会将当前被访问到的节点e,移动至内部的双向链表的尾部。

    void afterNodeAccess(Node<K,V> e) { // move node to lastLinkedHashMap.Entry<K,V> last;//原尾节点//如果accessOrder 是true ,且原尾节点不等于eif (accessOrder && (last = tail) != e) {//节点e强转成双向链表节点pLinkedHashMap.Entry<K,V> p =(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;//p现在是尾节点, 后置节点一定是nullp.after = null;//如果p的前置节点是null,则p以前是头结点,所以更新现在的头结点是p的后置节点aif (b == null)head = a;else//否则更新p的前直接点b的后置节点为 ab.after = a;//如果p的后置节点不是null,则更新后置节点a的前置节点为bif (a != null)a.before = b;else//如果原本p的后置节点是null,则p就是尾节点。 此时 更新last的引用为 p的前置节点blast = b;if (last == null) //原本尾节点是null  则,链表中就一个节点head = p;else {//否则 更新 当前节点p的前置节点为 原尾节点last, last的后置节点是pp.before = last;last.after = p;}//尾节点的引用赋值成ptail = p;//修改modCount。++modCount;}}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

值得注意的是,afterNodeAccess()函数中,会修改modCount,因此当你正在accessOrder=true的模式下,迭代LinkedHashMap时,如果同时查询访问数据,也会导致fail-fast,因为迭代的顺序已经改变。

7.2 containsValue

它重写了该方法,相比HashMap的实现,更为高效

    public boolean containsValue(Object value) {//遍历一遍链表,去比较有没有value相等的节点,并返回for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after) {V v = e.value;if (v == value || (value != null && value.equals(v)))return true;}return false;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

对比HashMap,是用两个for循环遍历,相对低效。

    public boolean containsValue(Object value) {Node<K,V>[] tab; V v;if ((tab = table) != null && size > 0) {for (int i = 0; i < tab.length; ++i) {for (Node<K,V> e = tab[i]; e != null; e = e.next) {if ((v = e.value) == value ||(value != null && value.equals(v)))return true;}}}return false;}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

8 遍历

重写了entrySet()如下:

    public Set<Map.Entry<K,V>> entrySet() {Set<Map.Entry<K,V>> es;//返回LinkedEntrySetreturn (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;}final class LinkedEntrySet extends AbstractSet<Map.Entry<K,V>> {public final Iterator<Map.Entry<K,V>> iterator() {return new LinkedEntryIterator();}}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

最终的EntryIterator:

    final class LinkedEntryIterator extends LinkedHashIteratorimplements Iterator<Map.Entry<K,V>> {public final Map.Entry<K,V> next() { return nextNode(); }}abstract class LinkedHashIterator {//下一个节点LinkedHashMap.Entry<K,V> next;//当前节点LinkedHashMap.Entry<K,V> current;int expectedModCount;LinkedHashIterator() {//初始化时,next 为 LinkedHashMap内部维护的双向链表的扁头next = head;//记录当前modCount,以满足fail-fastexpectedModCount = modCount;//当前节点为nullcurrent = null;}//判断是否还有nextpublic final boolean hasNext() {//就是判断next是否为null,默认next是head  表头return next != null;}//nextNode() 就是迭代器里的next()方法 。//该方法的实现可以看出,迭代LinkedHashMap,就是从内部维护的双链表的表头开始循环输出。final LinkedHashMap.Entry<K,V> nextNode() {//记录要返回的e。LinkedHashMap.Entry<K,V> e = next;//判断fail-fastif (modCount != expectedModCount)throw new ConcurrentModificationException();//如果要返回的节点是null,异常if (e == null)throw new NoSuchElementException();//更新当前节点为ecurrent = e;//更新下一个节点是e的后置节点next = e.after;//返回ereturn e;}//删除方法 最终还是调用了HashMap的removeNode方法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;}}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57

值得注意的就是:nextNode() 就是迭代器里的next()方法 。 
该方法的实现可以看出,迭代LinkedHashMap,就是从内部维护的双链表的表头开始循环输出。 
而双链表节点的顺序在LinkedHashMap增、删、改、查时都会更新。以满足按照插入顺序输出,还是访问顺序输出。

总结

LinkedHashMap相对于HashMap的源码比,是很简单的。因为大树底下好乘凉。它继承了HashMap,仅重写了几个方法,以改变它迭代遍历时的顺序。这也是其与HashMap相比最大的不同。 
在每次插入数据,或者访问、修改数据时,会增加节点、或调整链表的节点顺序。以决定迭代时输出的顺序。

  • accessOrder ,默认是false,则迭代时输出的顺序是插入节点的顺序。若为true,则输出的顺序是按照访问节点的顺序。为true时,可以在这基础之上构建一个LruCache.
  • LinkedHashMap并没有重写任何put方法。但是其重写了构建新节点的newNode()方法.在每次构建新节点时,将新节点链接在内部双向链表的尾部
  • accessOrder=true的模式下,在afterNodeAccess()函数中,会将当前被访问到的节点e,移动至内部的双向链表的尾部。值得注意的是,afterNodeAccess()函数中,会修改modCount,因此当你正在accessOrder=true的模式下,迭代LinkedHashMap时,如果同时查询访问数据,也会导致fail-fast,因为迭代的顺序已经改变。
  • nextNode() 就是迭代器里的next()方法 。 
    该方法的实现可以看出,迭代LinkedHashMap,就是从内部维护的双链表的表头开始循环输出。 
    而双链表节点的顺序在LinkedHashMap增、删、改、查时都会更新。以满足按照插入顺序输出,还是访问顺序输出。
  • 它与HashMap比,还有一个小小的优化,重写了containsValue()方法,直接遍历内部链表去比对value值是否相等。

转载自:面试必备:LinkedHashMap源码解析(JDK8)

这篇关于面试必备:LinkedHashMap源码解析(JDK8)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1050346

相关文章

网页解析 lxml 库--实战

lxml库使用流程 lxml 是 Python 的第三方解析库,完全使用 Python 语言编写,它对 XPath表达式提供了良好的支 持,因此能够了高效地解析 HTML/XML 文档。本节讲解如何通过 lxml 库解析 HTML 文档。 pip install lxml lxm| 库提供了一个 etree 模块,该模块专门用来解析 HTML/XML 文档,下面来介绍一下 lxml 库

字节面试 | 如何测试RocketMQ、RocketMQ?

字节面试:RocketMQ是怎么测试的呢? 答: 首先保证消息的消费正确、设计逆向用例,在验证消息内容为空等情况时的消费正确性; 推送大批量MQ,通过Admin控制台查看MQ消费的情况,是否出现消费假死、TPS是否正常等等问题。(上述都是临场发挥,但是RocketMQ真正的测试点,还真的需要探讨) 01 先了解RocketMQ 作为测试也是要简单了解RocketMQ。简单来说,就是一个分

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

秋招最新大模型算法面试,熬夜都要肝完它

💥大家在面试大模型LLM这个板块的时候,不知道面试完会不会复盘、总结,做笔记的习惯,这份大模型算法岗面试八股笔记也帮助不少人拿到过offer ✨对于面试大模型算法工程师会有一定的帮助,都附有完整答案,熬夜也要看完,祝大家一臂之力 这份《大模型算法工程师面试题》已经上传CSDN,还有完整版的大模型 AI 学习资料,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费

Java ArrayList扩容机制 (源码解读)

结论:初始长度为10,若所需长度小于1.5倍原长度,则按照1.5倍扩容。若不够用则按照所需长度扩容。 一. 明确类内部重要变量含义         1:数组默认长度         2:这是一个共享的空数组实例,用于明确创建长度为0时的ArrayList ,比如通过 new ArrayList<>(0),ArrayList 内部的数组 elementData 会指向这个 EMPTY_EL

如何在Visual Studio中调试.NET源码

今天偶然在看别人代码时,发现在他的代码里使用了Any判断List<T>是否为空。 我一般的做法是先判断是否为null,再判断Count。 看了一下Count的源码如下: 1 [__DynamicallyInvokable]2 public int Count3 {4 [__DynamicallyInvokable]5 get

工厂ERP管理系统实现源码(JAVA)

工厂进销存管理系统是一个集采购管理、仓库管理、生产管理和销售管理于一体的综合解决方案。该系统旨在帮助企业优化流程、提高效率、降低成本,并实时掌握各环节的运营状况。 在采购管理方面,系统能够处理采购订单、供应商管理和采购入库等流程,确保采购过程的透明和高效。仓库管理方面,实现库存的精准管理,包括入库、出库、盘点等操作,确保库存数据的准确性和实时性。 生产管理模块则涵盖了生产计划制定、物料需求计划、

OWASP十大安全漏洞解析

OWASP(开放式Web应用程序安全项目)发布的“十大安全漏洞”列表是Web应用程序安全领域的权威指南,它总结了Web应用程序中最常见、最危险的安全隐患。以下是对OWASP十大安全漏洞的详细解析: 1. 注入漏洞(Injection) 描述:攻击者通过在应用程序的输入数据中插入恶意代码,从而控制应用程序的行为。常见的注入类型包括SQL注入、OS命令注入、LDAP注入等。 影响:可能导致数据泄

从状态管理到性能优化:全面解析 Android Compose

文章目录 引言一、Android Compose基本概念1.1 什么是Android Compose?1.2 Compose的优势1.3 如何在项目中使用Compose 二、Compose中的状态管理2.1 状态管理的重要性2.2 Compose中的状态和数据流2.3 使用State和MutableState处理状态2.4 通过ViewModel进行状态管理 三、Compose中的列表和滚动