https://segmentfault.com/a/1190000012964859

LinkedHashMap 继承自 HashMap,在 HashMap 基础上,通过维护一条双向链表,解决了 HashMap 不能随时保持遍历顺序和插入顺序一致的问题。除此之外,LinkedHashMap 对访问顺序也提供了相关支持。在一些场景下,该特性很有用,比如缓存。在实现上,LinkedHashMap 很多方法直接继承自 HashMap,仅为维护双向链表覆写了部分方法。所以,要看懂 LinkedHashMap 的源码,需要先看懂 HashMap 的源码。

2. 原理

上一章说了 LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构。该结构由数组和链表或红黑树组成,结构示意图大致如下:

LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。其结构可能如下图:

上图中,淡蓝色的箭头表示前驱引用,红色箭头表示后继引用。每当有新键值对节点插入,新节点最终会接在 tail 引用指向的节点后面。而 tail 引用则会移动到新的节点上,这样一个双向链表就建立起来了。

上面的结构并不是很难理解,虽然引入了红黑树,导致结构看起来略为复杂了一些。但大家完全可以忽略红黑树,而只关注链表结构本身。好了,接下来进入细节分析吧。

3. 源码分析

3.1 Entry 的继承体系

在对核心内容展开分析之前,这里先插队分析一下键值对节点的继承体系。先来看看继承体系结构图:

上面的继承体系乍一看还是有点复杂的,同时也有点让人迷惑。HashMap 的内部类 TreeNode 不继承它的了一个内部类 Node,却继承自 Node 的子类 LinkedHashMap 内部类 Entry。这里这样做是有一定原因的,这里先不说。先来简单说明一下上面的继承体系。LinkedHashMap 内部类 Entry 继承自 HashMap 内部类 Node,并新增了两个引用,分别是 before 和 after。这两个引用的用途不难理解,也就是用于维护双向链表。同时,TreeNode 继承 LinkedHashMap 的内部类 Entry 后,就具备了和其他 Entry 一起组成链表的能力。但是这里需要大家考虑一个问题。当我们使用 HashMap 时,TreeNode 并不需要具备组成链表能力。如果继承 LinkedHashMap 内部类 Entry ,TreeNode 就多了两个用不到的引用,这样做不是会浪费空间吗?简单说明一下这个问题(水平有限,不保证完全正确),这里这么做确实会浪费空间,但与 TreeNode 通过继承获取的组成链表的能力相比,这点浪费是值得的。在 HashMap 的设计思路注释中,有这样一段话:

3.1 链表的建立过程

链表的建立过程是在插入键值对节点时开始的,初始情况下,让 LinkedHashMap 的 head 和 tail 引用同时指向新节点,链表就算建立起来了。随后不断有新节点插入,通过将新节点接在 tail 引用指向节点的后面,即可实现链表的更新。

Map 类型的集合类是通过 put(K,V) 方法插入键值对,LinkedHashMap 本身并没有覆写父类的 put 方法,而是直接使用了父类的实现。但在 HashMap 中,put 方法插入的是 HashMap 内部类 Node 类型的节点,该类型的节点并不具备与 LinkedHashMap 内部类 Entry 及其子类型节点组成链表的能力。那么,LinkedHashMap 是怎样建立链表的呢?在展开说明之前,我们先看一下 LinkedHashMap 插入操作相关的代码:

// HashMap 中实现public V put(K key, V value) {

return putVal(hash(key), key, value, false, true);

}

// HashMap 中实现

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) {...}

    // 通过节点 hash 定位节点所在的桶位置,并检测桶中是否包含节点引用

    if ((p = tab[i = (n - 1) & hash]) == 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) {...}

        else {

            // 遍历链表,并统计链表长度

            for (int binCount = 0; ; ++binCount) {

                // 未在单链表中找到要插入的节点,将新节点接在单链表的后面

                if ((e = p.next) == null) {

                    p.next = newNode(hash, key, value, null);

                    if (binCount >= TREEIFY_THRESHOLD - 1) {...}

                    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) {...}

            afterNodeAccess(e);    // 回调方法,后续说明

            return oldValue;

        }

    }

    ++modCount;

    if (++size > threshold) {...}

    afterNodeInsertion(evict);    // 回调方法,后续说明

    return null;

}

// HashMap 中实现Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {

    return new Node<>(hash, key, value, next);

}

// LinkedHashMap 中覆写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);

    // 将 Entry 接在双向链表的尾部

    linkNodeLast(p);

    return p;

}

// LinkedHashMap 中实现private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {

    LinkedHashMap.Entry<K,V> last = tail;

    tail = p;

    // last 为 null,表明链表还未建立

    if (last == null)

        head = p;

    else {

        // 将新节点 p 接在链表尾部

        p.before = last;

        last.after = p;

    }

}

上面就是 LinkedHashMap 插入相关的源码,这里省略了部分非关键的代码。我根据上面的代码,可以知道 LinkedHashMap 插入操作的调用过程。如下:

我把 newNode 方法红色背景标注了出来,这一步比较关键。LinkedHashMap 覆写了该方法。在这个方法中,LinkedHashMap 创建了 Entry,并通过 linkNodeLast 方法将 Entry 接在双向链表的尾部,实现了双向链表的建立。双向链表建立之后,我们就可以按照插入顺序去遍历 LinkedHashMap,大家可以自己写点测试代码验证一下插入顺序。

我们看一下插入后是怎么实现双向连接的

 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;

        }

}

两个全局变量head ,tail。head用于记录第一个put的Entry,tail用于记录最后一次put的Entry

第一次put:Entry1,按照方法linkNodeLast来运行

LinkedHashMap.Entry<K,V> last = tail = null;

        tail = p = Entry1;

        if (last == null)

            head = p = Entry1;

        else {

            p.before = last;

            last.after = p;

        }

那么结果就是 head = Entry1;  tail = Entry1; Entry1.before = null ,Entry1.last = null

第二次put:Entry2

LinkedHashMap.Entry<K,V> last = tail = Entry1;

        tail = p = Entry2;

        if (last == null)

            head = p;

        else {

            p.before = Entry2.before = last = Entry1;

            last.after = Entry1.after = Entry2;

        }

那么结果是 head = Entry1;tail = Entry2;

Entry1.after=Entry2;Entry2.before =Entry1;

.....以此类推

3.2 链表节点的删除过程

与插入操作一样,LinkedHashMap 删除操作相关的代码也是直接用父类的实现。在删除节点时,父类的删除逻辑并不会修复 LinkedHashMap 所维护的双向链表,这不是它的职责。那么删除及节点后,被删除的节点该如何从双链表中移除呢?当然,办法还算是有的。上一节最后提到 HashMap 中三个回调方法运行 LinkedHashMap 对一些操作做出响应。所以,在删除及节点后,回调方法 afterNodeRemoval 会被调用。LinkedHashMap 覆写该方法,并在该方法中完成了移除被删除节点的操作。相关源码如下:

// HashMap 中实现

public V remove(Object key) {

    Node<K,V> e;

    return (e = removeNode(hash(key), key, null, false, true)) == null ?

        null : e.value;

}

// HashMap 中实现

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) {...}

            else {

                // 遍历单链表,寻找要删除的节点,并赋值给 node 变量

                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) {...}

            // 将要删除的节点从单链表中移除

            else if (node == p)

                tab[index] = node.next;

            else

                p.next = node.next;

            ++modCount;

            --size;

            afterNodeRemoval(node);    // 调用删除回调方法进行后续操作

            return node;

        }

    }

    return null;

}

// LinkedHashMap 中覆写

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

    LinkedHashMap.Entry<K,V> p =

        (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;

    // 将 p 节点的前驱后后继引用置空

    p.before = p.after = null;

    // b 为 null,表明 p 是头节点

    if (b == null)

        head = a;

    else

        b.after = a;

    // a 为 null,表明 p 是尾节点

    if (a == null)

        tail = b;

    else

        a.before = b;

}

删除的过程并不复杂,上面这么多代码其实就做了三件事:

  1. 根据 hash 定位到桶位置
  2. 遍历链表或调用红黑树相关的删除方法
  3. 从 LinkedHashMap 维护的双链表中移除要删除的节点

举个例子说明一下,假如我们要删除下图键值为 3 的节点。

根据 hash 定位到该节点属于3号桶,然后在对3号桶保存的单链表进行遍历。找到要删除的节点后,先从单链表中移除该节点。如下:

然后再双向链表中移除该节点:

删除及相关修复过程并不复杂,结合上面的图片,大家应该很容易就能理解,这里就不多说了。

3.3 访问顺序的维护过程

前面说了插入顺序的实现,本节来讲讲访问顺序。默认情况下,LinkedHashMap 是按插入顺序维护链表。不过我们可以在初始化 LinkedHashMap,指定 accessOrder 参数为 true,即可让它按访问顺序维护链表。访问顺序的原理上并不复杂,当我们调用get/getOrDefault/replace等方法时,只需要将这些方法访问的节点移动到链表的尾部即可。相应的源码如下:

// LinkedHashMap 中覆写

public V get(Object key) {

    Node<K,V> e;

    if ((e = getNode(hash(key), key)) == null)

        return null;

    // 如果 accessOrder 为 true,则调用 afterNodeAccess 将被访问节点移动到链表最后

    if (accessOrder)

        afterNodeAccess(e);

    return e.value;

}

// LinkedHashMap 中覆写

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;

        // 如果 b 为 null,表明 p 为头节点

        if (b == null)

            head = a;

        else

            b.after = a;

        if (a != null)

            a.before = b;

        /*

         * 这里存疑,父条件分支已经确保节点 e 不会是尾节点,

         * 那么 e.after 必然不会为 null,不知道 else 分支有什么作用

         */

        else

            last = b;

        if (last == null)

            head = p;

        else {

            // 将 p 接在链表的最后

            p.before = last;

            last.after = p;

        }

        tail = p;

        ++modCount;

    }

}

上面就是访问顺序的实现代码,并不复杂。下面举例演示一下,帮助大家理解。假设我们访问下图键值为3的节点,访问前结构为:

访问后,键值为3的节点将会被移动到双向链表的最后位置,其前驱和后继也会跟着更新。访问后的结构如下:

3.4 基于 LinkedHashMap 实现缓存

前面介绍了 LinkedHashMap 是如何维护插入和访问顺序的,大家对 LinkedHashMap 的原理应该有了一定的认识。本节我们来写一些代码实践一下,这里通过继承 LinkedHashMap 实现了一个简单的 LRU 策略的缓存。在写代码之前,先介绍一下前置知识。

在3.1节分析链表建立过程时,我故意忽略了部分源码分析。本节就把忽略的部分补上,先看源码吧:

void afterNodeInsertion(boolean evict) { // possibly remove eldest

    LinkedHashMap.Entry<K,V> first;

    // 根据条件判断是否移除最近最少被访问的节点

    if (evict && (first = head) != null && removeEldestEntry(first)) {

        K key = first.key;

        removeNode(hash(key), key, null, false, true);

    }

}

// 移除最近最少被访问条件之一,通过覆盖此方法可实现不同策略的缓存protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {

return false;

}

上面的源码的核心逻辑在一般情况下都不会被执行,所以之前并没有进行分析。上面的代码做的事情比较简单,就是通过一些条件,判断是否移除最近最少被访问的节点。看到这里,大家应该知道上面两个方法的用途了。当我们基于 LinkedHashMap 实现缓存时,通过覆写removeEldestEntry方法可以实现自定义策略的 LRU 缓存。比如我们可以根据节点数量判断是否移除最近最少被访问的节点,或者根据节点的存活时间判断是否移除该节点等。本节所实现的缓存是基于判断节点数量是否超限的策略。在构造缓存对象时,传入最大节点数。当插入的节点数超过最大节点数时,移除最近最少被访问的节点。实现代码如下:

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

    private static final int MAX_NODE_NUM = 100;

    private int limit;

    public SimpleCache() {

        this(MAX_NODE_NUM);

    }

    public SimpleCache(int limit) {

        super(limit, 0.75f, true);

        this.limit = limit;

    }

    public V save(K key, V val) {

        return put(key, val);

    }

    public V getOne(K key) {

        return get(key);

    }

    public boolean exists(K key) {

        return containsKey(key);

    }
    /**

     * 判断节点数是否超限

     * @param eldest

     * @return 超限返回 true,否则返回 false

     */

    @Override

    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {

        return size() > limit;

    }

}

测试代码如下:

public class SimpleCacheTest {

    @Test

    public void test() throws Exception {

        SimpleCache<Integer, Integer> cache = new SimpleCache<>(3);

        for (int i = 0; i < 10; i++) {

            cache.save(i, i * i);

        }

        System.out.println("插入10个键值对后,缓存内容:");

        System.out.println(cache + "\n");

        System.out.println("访问键值为7的节点后,缓存内容:");

        cache.getOne(7);

        System.out.println(cache + "\n");

        System.out.println("插入键值为1的键值对后,缓存内容:");

        cache.save(1, 1);

        System.out.println(cache);

    }

}

测试结果如下:

在测试代码中,设定缓存大小为3。在向缓存中插入10个键值对后,只有最后3个被保存下来了,其他的都被移除了。然后通过访问键值为7的节点,使得该节点被移到双向链表的最后位置。当我们再次插入一个键值对时,键值为7的节点就不会被移除。

本节作为对前面内的补充,简单介绍了 LinkedHashMap 在其他方面的应用。本节内容及相关代码并不难理解,这里就不在赘述了。

 

LinkedHashmap简要说明的更多相关文章

  1. LinkedHashMap结构get和put源码流程简析及LRU应用

    原理这篇讲得比较透彻Java集合之LinkedHashMap. 本文属于源码阅读笔记,因put,get调用逻辑及链表维护逻辑复杂(至少网上其它文章的逻辑描述及配图,我都没看明白LinkedHashMa ...

  2. [Java] LinkedHashMap 源码简要分析

    特点 * 各个元素不仅仅按照HashMap的结构存储,而且每个元素包含了before/after指针,通过一个头元素header,形成一个双向循环链表.使用循环链表,保存了元素插入的顺序. * 可设置 ...

  3. Java 容器源码分析之 LinkedHashMap

    同 HashMap 一样,LinkedHashMap 也是对 Map 接口的一种基于链表和哈希表的实现.实际上, LinkedHashMap 是 HashMap 的子类,其扩展了 HashMap 增加 ...

  4. [Java] Hashtable 源码简要分析

    Hashtable /HashMap / LinkedHashMap 概述 * Hashtable比较早,是线程安全的哈希映射表.内部采用Entry[]数组,每个Entry均可作为链表的头,用来解决冲 ...

  5. Java 容器 LinkedHashMap源码分析1

    同 HashMap 一样,LinkedHashMap 也是对 Map 接口的一种基于链表和哈希表的实现.实际上, LinkedHashMap 是 HashMap 的子类,其扩展了 HashMap 增加 ...

  6. Java8集合框架——LinkedHashMap源码分析

    本文的结构如下: 一.LinkedHashMap 的 Javadoc 文档注释和简要说明 二.LinkedHashMap 的内部实现:一些扩展属性和构造函数 三.LinkedHashMap 的 put ...

  7. JavaScript权威设计--JavaScript函数(简要学习笔记十一)

    1.函数调用的四种方式 第三种:构造函数调用 如果构造函数调用在圆括号内包含一组实参列表,先计算这些实参表达式,然后传入函数内.这和函数调用和方法调用是一致的.但如果构造函数没有形参,JavaScri ...

  8. JavaScript权威设计--JavaScript函数(简要学习笔记十)

    1.函数命名规范 函数命名通常以动词为前缀的词组.通常第一个字符小写.当包含多个单词时,一种约定是将单词以下划线分割,就像"like_Zqz()". 还有一种就是"lik ...

  9. Java学习之LinkedHashMap学习总结

    前言: 在学习LRU算法的时候,看到LruCache源码实现是基于LinkedHashMap,今天学习一下LinkedHashMap的好处以及如何实现lru缓存机制的. 需求背景: LRU这个算法就是 ...

  10. 计算机程序的思维逻辑 (49) - 剖析LinkedHashMap

    之前我们介绍了Map接口的两个实现类HashMap和TreeMap,本节来介绍另一个实现类LinkedHashMap.它是HashMap的子类,但可以保持元素按插入或访问有序,这与TreeMap按键排 ...

随机推荐

  1. ROS应用层通信协议解析

    参考:http://wiki.ros.org/ROS/Master_API http://wiki.ros.org/ROS/Connection Header 说明 ROS本质上就是一个松耦合的通信框 ...

  2. 基于python的数学建模---二维插值的三维图

    import numpy as np from mpl_toolkits.mplot3d import Axes3D import matplotlib as mpl from scipy impor ...

  3. Dubbo-Activate实现原理

    前言 在Dubbo中有Filter使用,对于Filter来说我们会遇到这样的问题,Filter自身有很多的实现,我们希望某种条件下使用A实现,另外情况下使用B实现,这个时候我们前面介绍@SPI和@Ad ...

  4. winform datagridview行头添加序号

    1.使用datagirdview的RowPostPaint事件 2.datagirdview命名为dgv.(当然这个名字随意,开心就好) 3.贴代码 private void dgv_RowPostP ...

  5. 动态规划篇——线性DP

    动态规划篇--线性DP 本次我们介绍动态规划篇的线性DP,我们会从下面几个角度来介绍: 数字三角形 最长上升子序列I 最长上升子序列II 最长公共子序列 最短编辑距离 数字三角形 我们首先介绍一下题目 ...

  6. KVC原理与数据筛选

    作者:宋宏帅 1 前言 在技术论坛中看到一则很有意思的KVC案例: @interface Person : NSObject @property (nonatomic, copy) NSString ...

  7. Android ViewPager2 + TabLayout + BottomNavigationView

    Android ViewPager2 + TabLayout + BottomNavigationView 实际案例 本篇主要介绍一下 ViewPager2 + TabLayout + BottomN ...

  8. 微服务开发平台 Spring Cloud Blade 部署实践

    本文介绍使用 Rainbond 快速部署 Spring Cloud Blade 微服务平台.Spring Cloud Blade 是一个由商业级项目升级优化而来的微服务架构,采用Spring Boot ...

  9. virtualenv 配置(windows)

    1.在线安装 virtualenv pip install virtualenv 2.离线安装 下载virtualenv包,解压并进入setup.py所在文件夹中 python setup.py in ...

  10. uniapp微信小程序内部跳转其他微信小程序

    uniapp小程序内点击某个按钮跳转另外一个小程序连接,具体实现步骤如下: <view class="home-Item" @click="goNativeinde ...