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. Outlook API

    1.Outlook简介 若要从Outlook 外控制Outlook对象,必须在编写代码的工程中建立对Outlook对象库的引用. 1.1  Outlook Application说明: 代表整个Mic ...

  2. python3使用模块

    Python内置了很多非常有用的模块,只要安装完毕,这些模块就可以立刻使用. 我们以内建的sys模块为例,编写一个hello的模块: #!/usr/bin/env python3 # -*- codi ...

  3. Qt url中获取文件名

    QString filename = reply->rawHeader("Content-Disposition"); filename = filename.mid(QSt ...

  4. LeeCode——Combine Two Tables

    Table: Person +-------------+---------+ | Column Name | Type | +-------------+---------+ | PersonId ...

  5. mybatis if-else用法

    demo: <select id="queryRuralCodes" resultType="string" parameterType="ma ...

  6. Django框架(十九)--Django rest_framework-认证组件

    一.什么是认证 只有认证通过的用户才能访问指定的url地址,比如:查询课程信息,需要登录之后才能查看,没有登录,就不能查看,这时候需要用到认证组件 二.利用token记录认证过的用户 1.什么是tok ...

  7. 在centos8搭建dhcp服务

    1.用这两个命令就可以安装成功 yum - y install dhcp yum -y install dhcp-server.x86_64 2.配置文件在 /etc/dhcp/dhcpd.conf ...

  8. [LeetCode] 300. Longest Increasing Subsequence 最长递增子序列

    Given an unsorted array of integers, find the length of longest increasing subsequence. Example: Inp ...

  9. Spring Boot 2.2.0,性能提升+支持Java13

    随着 Spring Framework 5.2.0 成功发布之后,Spring Boot 2.2 也紧跟其后,发布了第一个版本:2.2.0.下面就来一起来看看这个版本都更新了些什么值得我们关注的内容. ...

  10. Linux性能优化实战学习笔记:第二十三讲

    一.索引节点和目录 1.索引节点 2.目录项 3.关系 为了帮助你理解目录项.索引节点以及文件数据的关系,我画了一张示意图,你可以对照这张图,来回忆刚刚讲过的内容,把只知识和细节串联起来 4.Slab ...