面试必备: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

相关文章

SQL 外键Foreign Key全解析

《SQL外键ForeignKey全解析》外键是数据库表中的一列(或一组列),用于​​建立两个表之间的关联关系​​,外键的值必须匹配另一个表的主键(PrimaryKey)或唯一约束(UniqueCo... 目录1. 什么是外键?​​ ​​​​2. 外键的语法​​​​3. 外键的约束行为​​​​4. 多列外键​

Java进行日期解析与格式化的实现代码

《Java进行日期解析与格式化的实现代码》使用Java搭配ApacheCommonsLang3和Natty库,可以实现灵活高效的日期解析与格式化,本文将通过相关示例为大家讲讲具体的实践操作,需要的可以... 目录一、背景二、依赖介绍1. Apache Commons Lang32. Natty三、核心实现代

使用Python自动化生成PPT并结合LLM生成内容的代码解析

《使用Python自动化生成PPT并结合LLM生成内容的代码解析》PowerPoint是常用的文档工具,但手动设计和排版耗时耗力,本文将展示如何通过Python自动化提取PPT样式并生成新PPT,同时... 目录核心代码解析1. 提取 PPT 样式到 jsON关键步骤:代码片段:2. 应用 JSON 样式到

Maven 插件配置分层架构深度解析

《Maven插件配置分层架构深度解析》:本文主要介绍Maven插件配置分层架构深度解析,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录Maven 插件配置分层架构深度解析引言:当构建逻辑遇上复杂配置第一章 Maven插件配置的三重境界1.1 插件配置的拓扑

全解析CSS Grid 的 auto-fill 和 auto-fit 内容自适应

《全解析CSSGrid的auto-fill和auto-fit内容自适应》:本文主要介绍了全解析CSSGrid的auto-fill和auto-fit内容自适应的相关资料,详细内容请阅读本文,希望能对你有所帮助... css  Grid 的 auto-fill 和 auto-fit/* 父元素 */.gri

Maven 依赖发布与仓库治理的过程解析

《Maven依赖发布与仓库治理的过程解析》:本文主要介绍Maven依赖发布与仓库治理的过程解析,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下... 目录Maven 依赖发布与仓库治理引言第一章:distributionManagement配置的工程化实践1

MySQL复合查询从基础到多表关联与高级技巧全解析

《MySQL复合查询从基础到多表关联与高级技巧全解析》本文主要讲解了在MySQL中的复合查询,下面是关于本文章所需要数据的建表语句,感兴趣的朋友跟随小编一起看看吧... 目录前言:1.基本查询回顾:1.1.查询工资高于500或岗位为MANAGER的雇员,同时还要满足他们的姓名首字母为大写的J1.2.按照部门

Spring三级缓存解决循环依赖的解析过程

《Spring三级缓存解决循环依赖的解析过程》:本文主要介绍Spring三级缓存解决循环依赖的解析过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、循环依赖场景二、三级缓存定义三、解决流程(以ServiceA和ServiceB为例)四、关键机制详解五、设计约

Redis实现分布式锁全解析之从原理到实践过程

《Redis实现分布式锁全解析之从原理到实践过程》:本文主要介绍Redis实现分布式锁全解析之从原理到实践过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、背景介绍二、解决方案(一)使用 SETNX 命令(二)设置锁的过期时间(三)解决锁的误删问题(四)Re

Android实现一键录屏功能(附源码)

《Android实现一键录屏功能(附源码)》在Android5.0及以上版本,系统提供了MediaProjectionAPI,允许应用在用户授权下录制屏幕内容并输出到视频文件,所以本文将基于此实现一个... 目录一、项目介绍二、相关技术与原理三、系统权限与用户授权四、项目架构与流程五、环境配置与依赖六、完整