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

相关文章

解析 XML 和 INI

XML 1.TinyXML库 TinyXML是一个C++的XML解析库  使用介绍: https://www.cnblogs.com/mythou/archive/2011/11/27/2265169.html    使用的时候,只要把 tinyxml.h、tinystr.h、tinystr.cpp、tinyxml.cpp、tinyxmlerror.cpp、tinyxmlparser.

Java面试八股之怎么通过Java程序判断JVM是32位还是64位

怎么通过Java程序判断JVM是32位还是64位 可以通过Java程序内部检查系统属性来判断当前运行的JVM是32位还是64位。以下是一个简单的方法: public class JvmBitCheck {public static void main(String[] args) {String arch = System.getProperty("os.arch");String dataM

电脑不小心删除的文件怎么恢复?4个必备恢复方法!

“刚刚在对电脑里的某些垃圾文件进行清理时,我一不小心误删了比较重要的数据。这些误删的数据还有机会恢复吗?希望大家帮帮我,非常感谢!” 在这个数字化飞速发展的时代,电脑早已成为我们日常生活和工作中不可或缺的一部分。然而,就像生活中的小插曲一样,有时我们可能会在不经意间犯下一些小错误,比如不小心删除了重要的文件。 当那份文件消失在眼前,仿佛被时间吞噬,我们不禁会心生焦虑。但别担心,就像每个问题

springboot家政服务管理平台 LW +PPT+源码+讲解

3系统的可行性研究及需求分析 3.1可行性研究 3.1.1技术可行性分析 经过大学四年的学习,已经掌握了JAVA、Mysql数据库等方面的编程技巧和方法,对于这些技术该有的软硬件配置也是齐全的,能够满足开发的需要。 本家政服务管理平台采用的是Mysql作为数据库,可以绝对地保证用户数据的安全;可以与Mysql数据库进行无缝连接。 所以,家政服务管理平台在技术上是可以实施的。 3.1

C++面试八股文:std::deque用过吗?

100编程书屋_孔夫子旧书网 某日二师兄参加XXX科技公司的C++工程师开发岗位第26面: 面试官:deque用过吗? 二师兄:说实话,很少用,基本没用过。 面试官:为什么? 二师兄:因为使用它的场景很少,大部分需要性能、且需要自动扩容的时候使用vector,需要随机插入和删除的时候可以使用list。 面试官:那你知道STL中的stack是如何实现的吗? 二师兄:默认情况下,stack使

高仿精仿愤怒的小鸟android版游戏源码

这是一款很完美的高仿精仿愤怒的小鸟android版游戏源码,大家可以研究一下吧、 为了报复偷走鸟蛋的肥猪们,鸟儿以自己的身体为武器,仿佛炮弹一样去攻击肥猪们的堡垒。游戏是十分卡通的2D画面,看着愤怒的红色小鸟,奋不顾身的往绿色的肥猪的堡垒砸去,那种奇妙的感觉还真是令人感到很欢乐。而游戏的配乐同样充满了欢乐的感觉,轻松的节奏,欢快的风格。 源码下载

tf.split()函数解析

API原型(TensorFlow 1.8.0): tf.split(     value,     num_or_size_splits,     axis=0,     num=None,     name='split' ) 这个函数是用来切割张量的。输入切割的张量和参数,返回切割的结果。  value传入的就是需要切割的张量。  这个函数有两种切割的方式: 以三个维度的张量为例,比如说一

Java面试八股之JVM参数-XX:+UseCompressedOops的作用

JVM参数-XX:+UseCompressedOops的作用 JVM参数-XX:+UseCompressedOops的作用是启用对象指针压缩(Ordinary Object Pointers compression)。这一特性主要应用于64位的Java虚拟机中,目的是为了减少内存使用。在传统的64位系统中,对象引用(即指针)通常占用8字节(64位),而大部分应用程序实际上并不需要如此大的地址空间

基于Java医院药品交易系统详细设计和实现(源码+LW+调试文档+讲解等)

💗博主介绍:✌全网粉丝10W+,CSDN作者、博客专家、全栈领域优质创作者,博客之星、平台优质作者、专注于Java、小程序技术领域和毕业项目实战✌💗 🌟文末获取源码+数据库🌟 感兴趣的可以先收藏起来,还有大家在毕设选题,项目以及论文编写等相关问题都可以给我留言咨询,希望帮助更多的人  Java精品实战案例《600套》 2023-2025年最值得选择的Java毕业设计选题大全:1000个热

工程文档CAD转换必备!在 Java 中将 DWG 转换为 JPG

Aspose.CAD 是一个独立的类库,以加强Java应用程序处理和渲染CAD图纸,而不需要AutoCAD或任何其他渲染工作流程。该CAD类库允许将DWG, DWT, DWF, DWFX, IFC, PLT, DGN, OBJ, STL, IGES, CFF2文件、布局和图层高质量地转换为PDF和光栅图像格式。 Aspose API支持流行文件格式处理,并允许将各类文档导出或转换为固定布局文件格