HashMap 有一个不足之处就是在迭代元素时与插入顺序不一致。而大多数人都喜欢按顺序做某些事情,所以,LinkedHashMap 就是针对这一点对 HashMap 进行扩展,主要新增了「两种迭代方式」

  • 按插入顺序 - 保证迭代元素的顺序与插入顺序一致
  • 按访问顺序 - 一种特殊的迭代顺序,从最近最少访问到最多访问的元素访问顺序,非常适合构建 LRU 缓存

LinkedHashMap 的重点就是这两点,搞清楚了,也就掌握了。由于它继承自 HashMap,所以在分析源码之前一定要先看看 HashMap 的源码,可查看此文《深入分析 JDK8 中 HashMap 的原理、实现和优化》

在 JDK 8 中,因为 LinkedHashMap 子类的存在,HashMap 引入的红黑树,在普通模式和树模式之间的转换变得复杂,那么它是如何设计和简化的呢?接下来看看 JDK 8 中的 LinkedHashMap 源码的设计和实现。

1. 按插入顺序迭代

HashMap 为什么在迭代时就杂乱无章了?看下它在迭代时获取下一个元素的方式,你就明白了:

final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next; // 下一个元素要访问节点
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null) throw new NoSuchElementException();
// 首先迭代自身所在链表
if ((next = (current = e).next) == null && (t = table) != null) {
// 如果链表访问结束,遍历哈希桶数组中下一个非空元素
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}

可以看到,HashMap 是按照哈希桶数组中非空的槽依次遍历的,再加上哈希冲突(链表)的情况,与插入顺序相比,更加乱了。

那 LinkedHashMap 怎么解决的?它维护了一个贯穿所有元素的双向链表,遍历时,迭代器直接对双向链表进行迭代,从而保证了与插入顺序一致,关键成员属性和节点信息定义如下:

// 双向链表头节点,也是最久没有访问的元素
transient LinkedHashMap.Entry<K,V> head;
// 双向链表尾节点,也是最近刚刚访问的元素
transient LinkedHashMap.Entry<K,V> tail;
// 迭代方式,true-按访问顺序迭代,false-按插入顺序迭代,默认 false
final boolean accessOrder;
// 添加了构建双向链表的前驱和后继指针
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 在元素插入、删除和访问时定义并调用了一些 Hook 方法,这些方法使得 LinkedHashMap 内部保持有序的机制相对独立,降低了普通模式和树模式转换的复杂度。

此外,红黑树节点继承的是 LinkedHashMap 的 Entry,固然可能多了两个指针,但在实现时有助于避免操作指针出现混淆错误。

LinkedHashMap 在元素插入时,覆盖的回调方法主要有 newNode, replacementNode, replacementTreeNode, newTreeNode,主要就是保持双向链表,核心是下面两个方法:

// 插入到双向链表的尾部
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;
}
} // 连接两个节点之间的前后指针
private void transferLinks(LinkedHashMap.Entry<K,V> src,
LinkedHashMap.Entry<K,V> dst) {
LinkedHashMap.Entry<K,V> b = dst.before = src.before;
LinkedHashMap.Entry<K,V> a = dst.after = src.after;
if (b == null)
head = dst;
else
b.after = dst;
if (a == null)
tail = dst;
else
a.before = dst;
}

迭代器的实现就比较简单了,直接对这个双向链表迭代即可,此时的迭代顺序就插入顺序。

2. 按访问顺序迭代

为了让元素按访问顺序排列,HashMap 定义了以下 Hook 方法,供 LinkedHashMap 实现:

void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }

各个方法实现的原理如下:

  • afterNodeAccess 的原理是:访问的元素如果不是尾节点,那么就把它与尾节点交换,所以随着元素的访问,访问次数越多的元素越靠后
  • afterNodeRemoval 这个没有特殊操作,正常的断开链条
  • afterNodeInsertion 的原理是:元素插入后,可能会删除最旧的、访问次数最少的元素,也就是头节点

重点看下插入之后的实现:

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);
}
}

是否会删除头节点,是由 removeEldestEntry 方法决定的,默认返回 false。在覆盖这个方法时,不能简单的返回 true,因为这样可能会导致一个空的 LinkedHashMap,通常的做法是在插入指定数量的元素后再删除,具体见下面 LRU 缓存的实现。

3. 实现一个 LRU 缓存

借助 LinkedHashMap 可以很方便的实现一个 LRU 缓存数据结构,只需设置 accessOrder 为 true,并覆盖 removeEldestEntry 方法即可,代码如下:

final int MAX_CACHE_SIZE = 100;
LinkedHashMap<Object, Object> lru = new LinkedHashMap<Object, Object>(MAX_CACHE_SIZE, 0.75f, true) {
private static final long serialVersionUID = 1L;
protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
return size() > MAX_CACHE_SIZE;
}
};

4. 小结

LinkedHashMap 代码比较简单,难的都被 HashMap 实现了:),它们的异同点分别如下:

  • 底层都是数组+链表+红黑树(废话)
  • 迭代器都是快速失败的,都是非线程安全
  • LinkedHashMap 有按插入和访问两种迭代顺序,而HashMap 乱序,迭代顺序不可预测

看这个类的源码,最主要的还是看 HashMap 定义 Hook 方法,使得 LinkedHashMap 保持有序的机制相对独立的设计,这是模板模式的应用。

LinkedHashMap 的核心就 2 点,搞清楚,也就掌握了的更多相关文章

  1. AJ学IOS(41)UI之核心动画 两行代码搞定3D转场

    AJ分享,必须精品 效果: 代码: 其实代码很少,苹果都给封装好了 // 1.创建核心动画 CATransition *ca = [CATransition animation]; // 1.1动画过 ...

  2. JDK源码分析(6)之 LinkedHashMap 相关

    LinkedHashMap实质是HashMap+LinkedList,提供了顺序访问的功能:所以在看这篇博客之前最好先看一下我之前的两篇博客,HashMap 相关 和 LinkedList 相关: 一 ...

  3. 框架源码系列八:Spring源码学习之Spring核心工作原理(很重要)

    目录:一.搞清楚ApplicationContext实例化Bean的过程二.搞清楚这个过程中涉及的核心类三.搞清楚IOC容器提供的扩展点有哪些,学会扩展四.学会IOC容器这里使用的设计模式五.搞清楚不 ...

  4. JDK1.8源码逐字逐句带你理解LinkedHashMap底层

    注意 我希望看这篇的文章的小伙伴如果没有了解过HashMap那么可以先看看我这篇文章:http://blog.csdn.net/u012403290/article/details/65442646, ...

  5. linkedHashMap源码解析(JDK1.8)

    引言 关于java中的不常见模块,让我一下子想我也想不出来,所以我希望以后每次遇到的时候我就加一篇.上次有人建议我写全所有常用的Map,所以我研究了一晚上LinkedHashMap,把自己感悟到的解释 ...

  6. 集合系列 Map(十三):LinkedHashMap

    我们之前说过 LinkedHashMap 是在 HashMap 的基础上,增加了对插入元素的链表维护.那么其到底是怎么实现的呢?今天这篇文章就带我们来一探究竟. public class Linked ...

  7. PPT鼠绘必须掌握的PPT绘图三大核心功能

    在PPT制作教程栏目中,陆陆续续的分享了一系列通过合并形状功能来绘图的教程,绘制安卓机器人.绘制西瓜.绘制鸡蛋.其实,合并形状功能只是PPT绘图的一部分,而真正想要掌握PPT鼠绘,仅仅是会使用合并形状 ...

  8. 最快让你上手ReactiveCocoa之基础篇

    前言 很多blog都说ReactiveCocoa好用,然后各种秀自己如何灵活运用ReactiveCocoa,但是感觉真正缺少的是一篇如何学习ReactiveCocoa的文章,这里介绍一下. 1.Rea ...

  9. 爆料喽!!!开源日志库Logger的剖析分析

    导读 Logger类提供了多种方法来处理日志活动.上一篇介绍了开源日志库Logger的使用,今天我主要来分析Logger实现的原理. 库的整体架构图 详细剖析 我们从使用的角度来对Logger库抽茧剥 ...

随机推荐

  1. 什么是唯品会JIT业务

    以销定采的模式,供应商将商品发给唯品会仓库在由唯品会发给客户:首先在唯品会创建档期绑定PO此时设置的商品库存为虚拟库存,之后供应商根据实际产生的有效订单将订单中的商品发给唯品会,最后再由唯品会发给用户 ...

  2. Python 定时调度

    APScheduler APScheduler是基于Quartz的一个Python定时任务框架,实现了Quartz的所有功能,使用起来十分方便.提供了基于日期.固定时间间隔以及crontab类型的任务 ...

  3. Django admin简单介绍

    生成同步数据库的脚本: python manage.py makemigrations 同步数据库: python manage.py migrate 创建后台用户 python manage.py ...

  4. windows 如何cmd启动redis

    运行cmd 然后到redis路径 运行命令: redis-server redis.windows.conf

  5. Nginx+Docker部署模式下 asp.net core 获取真实的客户端ip

    目录 Nginx+Docker部署模式下 asp.net core 获取真实的客户端ip 场景 过程还原 结论 参考资料 Nginx+Docker部署模式下 asp.net core 获取真实的客户端 ...

  6. Linux rpm包管理工具

    1.什么是rpm包 是一种编译好的二进制软件包,安装速度快. 2.rpm包命名 el6:软件包用于在Red Hat 6.x, CentOS 6.x, and CloudLinux 6.x进行安装 el ...

  7. 程序员代码面试指南 IT名企算法与数据结构题目最优解

    原文链接 这是一本程序员面试宝典!书中对IT名企代码面试各类题目的最优解进行了总结,并提供了相关代码实现.针对当前程序员面试缺乏权威题目汇总这一痛点,本书选取将近200道真实出现过的经典代码面试题,帮 ...

  8. Rust中的Trait

    类似接口,但和php中的trait又有点不一样. pub trait Summary { fn summarize(&self) -> String; } pub struct NewA ...

  9. JS高阶---函数的prototype

    思维导图 栈堆翻译为为stack (1)原型与原型链 概念一.原型对象 验证步骤: 1.打印Data函数的原型prototype 原型属性指向原型对象 ===ES源码结构分析示意=== 2.空对象 3 ...

  10. 12-numpy笔记-莫烦基本操作2

    代码 import numpy as np A = np.arange(3,15) print('-1-') print(A) print('-2-') print(A[3]) A = np.aran ...