LinkedHashMap 的核心就 2 点,搞清楚,也就掌握了
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 点,搞清楚,也就掌握了的更多相关文章
- AJ学IOS(41)UI之核心动画 两行代码搞定3D转场
AJ分享,必须精品 效果: 代码: 其实代码很少,苹果都给封装好了 // 1.创建核心动画 CATransition *ca = [CATransition animation]; // 1.1动画过 ...
- JDK源码分析(6)之 LinkedHashMap 相关
LinkedHashMap实质是HashMap+LinkedList,提供了顺序访问的功能:所以在看这篇博客之前最好先看一下我之前的两篇博客,HashMap 相关 和 LinkedList 相关: 一 ...
- 框架源码系列八:Spring源码学习之Spring核心工作原理(很重要)
目录:一.搞清楚ApplicationContext实例化Bean的过程二.搞清楚这个过程中涉及的核心类三.搞清楚IOC容器提供的扩展点有哪些,学会扩展四.学会IOC容器这里使用的设计模式五.搞清楚不 ...
- JDK1.8源码逐字逐句带你理解LinkedHashMap底层
注意 我希望看这篇的文章的小伙伴如果没有了解过HashMap那么可以先看看我这篇文章:http://blog.csdn.net/u012403290/article/details/65442646, ...
- linkedHashMap源码解析(JDK1.8)
引言 关于java中的不常见模块,让我一下子想我也想不出来,所以我希望以后每次遇到的时候我就加一篇.上次有人建议我写全所有常用的Map,所以我研究了一晚上LinkedHashMap,把自己感悟到的解释 ...
- 集合系列 Map(十三):LinkedHashMap
我们之前说过 LinkedHashMap 是在 HashMap 的基础上,增加了对插入元素的链表维护.那么其到底是怎么实现的呢?今天这篇文章就带我们来一探究竟. public class Linked ...
- PPT鼠绘必须掌握的PPT绘图三大核心功能
在PPT制作教程栏目中,陆陆续续的分享了一系列通过合并形状功能来绘图的教程,绘制安卓机器人.绘制西瓜.绘制鸡蛋.其实,合并形状功能只是PPT绘图的一部分,而真正想要掌握PPT鼠绘,仅仅是会使用合并形状 ...
- 最快让你上手ReactiveCocoa之基础篇
前言 很多blog都说ReactiveCocoa好用,然后各种秀自己如何灵活运用ReactiveCocoa,但是感觉真正缺少的是一篇如何学习ReactiveCocoa的文章,这里介绍一下. 1.Rea ...
- 爆料喽!!!开源日志库Logger的剖析分析
导读 Logger类提供了多种方法来处理日志活动.上一篇介绍了开源日志库Logger的使用,今天我主要来分析Logger实现的原理. 库的整体架构图 详细剖析 我们从使用的角度来对Logger库抽茧剥 ...
随机推荐
- Qt固定窗口大小
指定大小 this->setMaximumSize(250, 250); 默认大小 this->setMaximumSize(this->width(), this->heig ...
- 网络远程唤醒 WOL Magic Packet【转】
转自:https://www.cnblogs.com/dcb3688/p/4608063.html Magic Packet Magic Packet白皮书介绍: The basic technica ...
- constant timer(固定定时器),constant throughput timer(常数吞吐量定时器);多个请求,某个请求a下,设置常数吞吐量定时器,模式:all active threads(shared)则所有请求吞吐量一致;
1.两请求之间添加'固定定时器' 1000ms,那么两请求发送间隔时间是多少? 1000ms吗? 由实验得出,2个请求发送间隔时间 = 1000ms + 第一个请求时间(发出至完成后时间) 2.单个请 ...
- NOIP 2002 过河卒
洛谷P1002 https://www.luogu.org/problemnew/show/P1002 JDOJ 1290 https://neooj.com/oldoj/problem.php?id ...
- Redis和MySQL数据同步及Redis使用场景
1.同步MySQL数据到Redis (1) 在redis数据库设置缓存时间,当该条数据缓存时间过期之后自动释放,去数据库进行重新查询,但这样的话,我们放在缓存中的数据对数据的一致性要求不是很高才能放入 ...
- zeebe prometheus 监控配置
zeebe 默认已经集成了prometheus,以下是一个简单的配置,关于grafana 的集成需要调整下 dashboard,目前网上的已经太老了 docker-compose 文件 versi ...
- loj2305 NOI2017 游戏
题目链接 思路 既然\(x\)的数量那么小,我们就可以先把每个\(x\)搜索一遍. 枚举x的时候不需要把\(a,b,c\)全枚举一遍,只要枚举其中的两个就可以枚举到当前位置选任何车的情况. 然后就变成 ...
- 【UVA1303】Wall(凸包)
点此看题面 大致题意: 给你一个多边形,要求建一面墙使得墙上的点至少离多边形每个顶点\(R\)的距离,求最短的墙长. 考虑\(R=0\) 考虑当\(R=0\)时,所求的答案显然就是求得的凸包的周长. ...
- Linux性能优化实战学习笔记:第二十七讲
一.案例环境描述 1.环境准备 2CPU,4GB内存 预先安装docker sysstat工具 2.温馨提示 案例中 Python 应用的核心逻辑比较简单,你可能一眼就能看出问题,但实际生产环境中的源 ...
- 原生/CM系列网络叹号问题
网络叹号问题: 原因:某墙屏蔽了谷歌,而原生安卓是利用谷歌的服务器来测试网络是否通畅的 解决方案:修改网络测试的服务器地址 方案来源:https://www.noisyfox.io/android-c ...