Java 容器源码分析之 LinkedHashMap
同 HashMap 一样,LinkedHashMap 也是对 Map 接口的一种基于链表和哈希表的实现。实际上, LinkedHashMap 是 HashMap 的子类,其扩展了 HashMap 增加了双向链表的实现。相较于 HashMap 的迭代器中混乱的访问顺序,LinkedHashMap 可以提供可以预测的迭代访问,即按照插入序 (insertion-order) 或访问序 (access-order) 来对哈希表中的元素进行迭代。
1 |
public class LinkedHashMap<K,V> |
从类声明中可以看到,LinkedHashMap 确实是继承了 HashMap,因而 HashMap 中的一些基本操作,如哈希计算、扩容、查找等,在 LinkedHashMap 中都和父类 HashMap 是一致的。
但是,和 HashMap 有所区别的是,LinkedHashMap 支持按插入序 (insertion-order) 或访问序 (access-order) 来访问其中的元素。所谓插入顺序,就是 Entry 被添加到 Map 中的顺序,更新一个 Key 关联的 Value 并不会对插入顺序造成影响;而访问顺序则是对所有 Entry 按照最近访问 (least-recently) 到最远访问 (most-recently) 进行排序,读写都会影响到访问顺序,但是对迭代器 (entrySet(), keySet(), values()) 的访问不会影响到访问顺序。访问序的特性使得可以很容易通过 LinkedHashMap 来实现一个 LRU(least-recently-used) Cache,后面会给出一个简单的例子。
之所以 LinkedHashMap 能够支持插入序或访问序的遍历,是因为 LinkedHashMap 在 HashMap 的基础上增加了双向链表的实现,下面会结合 JDK 8 的源码进行简要的分析。
底层结构
LinkedHashMap 是 HashMap 的子类,因而 HashMap 中的成员在 LinkedHashMap 中也存在,如底层的 table 数组等,这里就不再说明了。我们重点关注一下 LinkedHashMap 中节点发生的变化。
1 |
/** |
为了实现双向链表,LinkedHashMap 的节点在父类的基础上增加了 before/after 引用,并且使用 head 和 tail 分别保存双向链表的头和尾。同时,增加了一个标识来保存 LinkedHashMap 的迭代顺序是插入序还是访问序。
由于父类 HashMap 的节点中存在 next 引用,可以将每个桶中的元素都当作一个单链表看待;LinkedHashMap 的每个桶中当然也保留了这个单链表关系,不过这个关系由父类进行管理,LinkedHashMap 中只会对双向链表的关系进行管理。LinkedHashMap 中所有的元素都被串联在一个双向链表中。
双向链表
为了简化对双向链表的操作,LinkedHashMap 中提供了 linkNodeLast 和 transferLinks 方法,分别如下:
1 |
// link at the end of list |
LinkedHashMap 重写了父类新建节点的方法,在新建节点之后调用 linkNodeLast 方法将新添加的节点链接到双向链表的末尾:
1 |
//覆盖父类方法 |
我们知道,HashMap 中单个桶中的元素可能会在单链表和红黑树之间进行转换,LinkedHashMap 中当然也是一样,不过在转换时还要调用 transferLinks 来改变双向链表中的连接关系:
1 |
//覆盖父类方法 |
如何维护插入序和访问序?
在 LinkedHashMap 中,所有的 Entry 都被串联在一个双向链表中。从上一节的代码中可以看到,每次在新建一个节点时都会将新建的节点链接到双向链表的末尾。这样从双向链表的尾部向头部遍历就可以保证插入顺序了,头部节点是最早添加的节点,而尾部节点则是最近添加的节点。那么,访问顺序要怎么实现呢?
之前我们在分析 HashMap 的源码的时候,在添加及更新、查找、删除等操作中可以看到 afterNodeAccess、afterNodeInsertion、afterNodeRemoval 等几个方法的调用,不过在 HashMap 中这几个方法中没有任何操作。实际上,这几个方法就是供 LinkedHashMap 的重写的,我们不妨看一下在 HashMap 中这几个方法的声明:
1 |
// Callbacks to allow LinkedHashMap post-actions |
在 LinkedHashMap 中对这几个方法进行了重写:
1 |
//移除节点的回调函数 |
在插入节点、删除节点和访问节点后会调用相应的回调函数。可以看到,在 afterNodeAccess方法中,如果该 LinkedHashMap 是访问序,且当前访问的节点不是尾部节点,则该节点会被置为双链表的尾节点。即,在访问序下,最近访问的节点会是尾节点,头节点则是最远访问的节点。
在 afterNodeInsertion 中,如果 removeEldestEntry(first) 节点返回 true,则会将头部节点删除。如果想要实现一个固定容量的 Map,可以在继承 LinkedHashMap 后重写 removeEldestEntry 方法。在 LinkedHashMap 中,该方法始终返回 false。
1 |
//返回false |
在 HashMap 中,在 putVal 和 removeNode 中都调用了相应的回调函数,而 get 则没有,因而在 LinkedHahsMap 中进行了重写:
1 |
public V get(Object key) {
|
遍历及迭代器
因为 LinkeHashMap 的所有的节点都在一个双向链表中,因而可以通过该双向链表来遍历所有的 Entry。而在 HashMap 中,要遍历所有的 Entry,则要依次遍历所有桶中的单链表。相比较而言,从时间复杂度的角度来看,LinkedHashMap 的复杂度为 O(size()),而 HashMap 则为 O(capacity + size())。
1 |
//因为所有的节点都被串联在双向链表中,迭代器在迭代时可以利用双向链表的链接关系进行 |
可以看到,在遍历所有节点时是通过节点的 after 引用进行的。这样,可以双链表的头部遍历到到双链表的尾部,就不用像 HahsMap 那样访问空槽了。
在 containsValue 和 internalWriteEntries 中也使用了双向链表进行遍历。
1 |
public boolean containsValue(Object value) {
|
使用 LinkedHashMap 实现 LRU Cache
LinkedHashMap 的访问序可以方便地用来实现一个 LRU Cache。在访问序模式下,尾部节点是最近一次被访问的节点 (least-recently),而头部节点则是最远访问 (most-recently) 的节点。因而在决定失效缓存的时候,将头部节点移除即可。
但是,由于链表是无界的,但缓存往往是资源受限的,如何确定何时移除最远访问的缓存呢?前面分析过,在 afterNodeInsertion 中,会调用 removeEldestEntry 来决定是否将最老的节点移除,因而我们可以使用 LinkedHashMap 的子类,并重写 removeEldestEntry 方法,当 Enrty 的数量超过缓存的容量是返回 true 即可。
下面给出基于 LinkedHashMap 实现的 LRU Cache 的代码:
public class CacheImpl<K,V> {
private Map<K, V> cache;
private int capacity;
public enum POLICY {
LRU, FIFO
}
public CacheImpl(int cap, POLICY policy) {
this.capacity = cap;
cache = new LinkedHashMap<K, V>(cap, 0.75f, policy.equals(POLICY.LRU)){
//超出容量就删除最老的值
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
};
}
public V get(K key) {
if (cache.containsKey(key)) {
return cache.get(key);
}
return null;
}
public void set(K key, V val) {
cache.put(key, val);
}
public void printKV() {
System.out.println("key value in cache");
for (Map.Entry<K,V> entry : cache.entrySet()) {
System.out.println(entry.getKey() + ":" + entry.getValue());
}
}
public static void main(String[] args) {
CacheImpl<Integer, String> cache = new CacheImpl(5, POLICY.LRU);
cache.set(1, "first");
cache.set(2, "second");
cache.set(3, "third");
cache.set(4, "fourth");
cache.set(5, "fifth");
cache.printKV();
cache.get(1);
cache.get(2);
cache.printKV();
cache.set(6, "sixth");
cache.printKV();
}
}
小结
本文对 JDK 8 中的 LinkedHashMap 的源码及实现进行了简单的分析。LinkedHashMap 继承自 HashMap,并在其基本结构上增加了双向链表的实现,因而 LinkedHashMap 在内存占用上要比 HashMap 高出许多。LinkedHashMap 仍然沿用了 HashMap 中基于桶数组、桶内单链表和红黑树结构的哈希表,在哈希计算、定位、扩容等方面都和 HashMAp 是一致的。LinkedHashMap 同样支持为 null 的键和值。
由于增加了双向链表将所有的 Entry 串在一起,LinkedHashMap 的一个重要的特点就是支持按照插入顺序或访问顺序来遍历所有的 Entry,这一点和 HashMap 的乱序遍历很不相同。在一些对顺序有要求的场合,就需要使用 LinkedHashMap 来替代 HashMap。
由于双向链表的缘故,在遍历时可以直接在双向链表上进行,因而遍历时间复杂度和容量无关,只和当前 Entry 数量有关。这点相比于 HashMap 要更加高效一些。
Java 容器源码分析之 LinkedHashMap的更多相关文章
- 基于JDK1.8,Java容器源码分析
容器源码分析 如果没有特别说明,以下源码分析基于 JDK 1.8. 在 IDEA 中 double shift 调出 Search EveryWhere,查找源码文件,找到之后就可以阅读源码. Lis ...
- Java 容器源码分析之1.7HashMap
以下内容基于jdk1.7.0_79源码: 什么是HashMap 基于哈希表的一个Map接口实现,存储的对象是一个键值对对象(Entry<K,V>): HashMap补充说明 基于数组和链表 ...
- Java 容器源码分析之Map-Set-List
HashMap 的实现原理 HashMap 概述 HashMap 是基于哈希表的 Map 接口的非同步实现.此实现提供所有可选的映射操作,并允许使用 null 值和 null 键.此类不保证映射的顺序 ...
- java容器源码分析及常见面试题笔记
概览 容器主要包括 Collection 和 Map 两种,Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表. List Arraylist: Object数组 ...
- Java 容器源码分析之 ArrayList
概览 ArrayList是最常使用的集合类之一了.在JDK文档中对ArrayList的描述是:ArrayList是对list接口的一种基于可变数组的实现.ArrayList类的声明如下: 12 pub ...
- Java 容器源码分析之 TreeMap
TreeMap 是一种基于红黑树实现的 Key-Value 结构.在使用集合视图在 HashMap 中迭代时,是不能保证迭代顺序的: LinkedHashMap 使用了双向链表,保证按照插入顺序或者访 ...
- Java 容器源码分析之ConcurrentHashMap
深入浅出ConcurrentHashMap(1.8) 前言 HashMap是我们平时开发过程中用的比较多的集合,但它是非线程安全的,在涉及到多线程并发的情况,进行put操作有可能会引起死循环,导致CP ...
- Java 容器源码分析之1.8HashMap方法讲解
前言:Java8之后新增挺多新东西,在网上找了些相关资料,关于HashMap在自己被血虐之后痛定思痛决定整理一下相关知识方便自己看.图和有些内容参考的这个文章:http://www.importnew ...
- Java 容器源码分析之 Map
ava.util 中的集合类包含 Java 中某些最常用的类.最常用的集合类是 List 和 Map.List 的具体实现包括 ArrayList 和 Vector,它们是可变大小的列表,比较适合构建 ...
随机推荐
- Linux环境下Flask部署至apache
https://blog.csdn.net/rainbowlemonade/article/details/79725328
- 《Linux就该这么学》第十八天课程
1.使用MariaDB数据库管理系统 今天没什么笔记,就不发了.想深入学习的可以前往原创地址:https://www.linuxprobe.com/chapter-18.html 图18-1 Mari ...
- 7.地图随机装饰,与转化过程补充,与ai的设计思路
这两天本来只想实现地图的随机装饰,然后发现以前的bin格式设计存在不足,所以最后不得不去改地图,并去重制整个地图的阶段,此篇总结这个过程 先描述下bin结构 首先地图由无数六边形组合,一个六边形由两层 ...
- Win7 VS2017编译Blender2.79
去年在VS2013环境编译过一次,重装系统后换了VS2017,正好刚编译完Godot3.0.2,顺手把Blender也编译了吧. 官方Windows下编译指南 https://wiki.blender ...
- Python3学习(1)——初步了解
Python是解析性语言,Python解释器将源程序解释并执行. 基本语法 print() --打印字符串 -直接打印 print("hello world") 结果: hel ...
- CentOS 7 rpm安装jdk
RPM 安装jdk1.8.0_111 ,查询系统自带的jdk rpm -qa | grep java 查询结果如下: [root@bogon ~]# rpm -qa | grep java javap ...
- 探寻TP-Link路由器的登录验证
提示:该案例仅供学习使用,切勿滥用!!! 查找路由器连接地址 查找ip $ ifconfig enp2s0: flags=<UP,BROADCAST,RUNNING,MULTICAST> ...
- 与我们息息相关的internet服务(3)---电子邮件服务
几年前了解了一下,现在再实施的时候,再了解,当然如果要到牛人张小龙28岁时的开发程度,可能还差一个筋斗云 在起步一个公司,从组建的技术上,可能要准备很多东西,其中一个就是我们熟悉的企业邮箱. 伊妹儿, ...
- Spring使用Autowiring自动装配 解决提示报错小技巧
1.打开Settings 输入Inspections 找到Spring --> Spring Core --> Code --> Autowiring for Bean ...
- Django 执行单独脚本及SyntaxError缩进报错解决
有时候会碰到这样的场景,对于一些业务升级,我需要把数据库数据做些处理,同时又想以 Django 项目的环境变量执行脚本,这个时候使用 python 脚本是再适合不过的手段了. 注意:在pycharm里 ...