Map随笔:有序的HashMap——LinkedHashMap
Map随笔:有序的HashMap——LinkedHashMap
一,概述
LinkedHashMap继承于HashMap(笔者另一篇分享HashMap的博文),它的特点在于它的有序性。底层采用双向链表来实现数据节点的顺序性。LinkedHashMap的有序性分成两种,一种是输出顺序可以是插入的顺序,另一种顺序便是将最新操作的数据放在内部链表的尾部,可以用来做LRU算法(文中会详解)。
二,源码结构
1,属性
//链表的头部
transient LinkedHashMap.Entry<K,V> head;
//链表的尾部
transient LinkedHashMap.Entry<K,V> tail;
//存取数据后是否把数据放在尾部,该属性是LinkedHashMap实现LRU(最少使用)算法的关键
//默认都是false
final boolean accessOrder;
2,重要的内部类
static class Entry<K,V> extends HashMap.Node<K,V> {
//before:上一个节点,after:下一个节点
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
LinkedHashMap存储数据的载体Entry继承了HashMap中的Node,但是多了before和after两个属性,这两个属性是的多个Entry连成一条链表,这也时LinkedHashMap有序性的原理。
3,构造器
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
public LinkedHashMap() {
super();
accessOrder = false;
}
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super();
accessOrder = false;
putMapEntries(m, false);
}
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
因为LinkedHashMap是继承HashMap的,所以构造器基本上都是类似的,无非不同的就是它多了一个属性accessOrder来控制内部排序的方式,可以看出默认都是fasle,如果要更改内部的默认的排序方式,可以使用三参的这个构造器。
4,核心方法
1,新建节点
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);
linkNodeLast(p);
return p;
}
//将节点添加到双向链表上
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;
tail = p;
//如果链表尾部数据时null,则说明这个链表上没有数据,则当前新插入的数据便是链表的头部节点
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
}
LinkedHashMap重写了HashMap的新建节点方法,在原先基础上只是将新的节点添加到内部链表上而已,所以代码相对简单。
2,节点插入成功后的扩展方法
看过HashMap(Java8)源码的同学应该应该对这个方法有一些印象,如下,HashMap的put方法在新增数据之后,会先调用afterNodeInsertion方法,这也是HashMap预留给LinkedHashMap的可扩展点,像这种扩展点一共三处,另外两处,一个也是在putVal方法的数据修改后,另一个是在删除节点后。
// put数据
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
...................................
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//新增覆盖后
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
resize();
//新增数据后
afterNodeInsertion(evict);
return null;
}
// 删除节点
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
...................................
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
else if (node == p)
tab[index] = node.next;
else
p.next = node.next;
++modCount;
--size;
//节点删除后
afterNodeRemoval(node);
return node;
}
}
return null;
}
分开看一下这三个留给LinkedHashMap的扩展方法它都做了什么。首先afterNodeInsertion()——当数据插入后,因为LinkedHashMap比HashMap内部多了一个维持顺序的双向链表,所以不难猜出都干了什么,这么想,如果我们自己实现一个缓存,首先要解决的便是这个缓存空间满了怎么处理,第一想到的便是删除一些数据,那怎么删除,删除哪些数据,什么时候删除呢?用过Redis的都知道Redis采用的时LRU算法,当Redis的内存小于一点大小后,便会删除一些不常用的数据(Redis采用的也不是真正的LRU,具体这块不做过多研究),由Redis的理念我们便可以利用LinkedHashMap来实现我们自己定义的缓存了,首先怎么删除数据我们是不用考虑的,如下所示源码,删除是调用的HashMap的方法。下来就是删除哪些数据,由源码可以,每次删除的时顺序链表的头部的那个节点,而头部节点是什么数据,这个控制逻辑不在这个方法中定义,是在afterNodeAccess这个方法中吗,我们稍后在分析,剩下的就只有是什么时候删除这个问题,看源码,作者将什么时候删除这个问题的逻辑封装到了removeEldestEntry这个方法中,而这个方法我们是可以重写的,这对我们是非常友好的,我们可以自定义这个逻辑,最的简单的,我们写一段伪代码来讲(如下3),我们可以设计当LinkedHashMap容量小于10时,删除节点,当然具体逻辑可以根据业务去设计。虽然说Java提供了LinkedHashMap的这个功能,但是基本上,工作中我们用的很少,但这块了解了解也不是什么坏事。
//1,当数据插入后
//evict:是否允许改变数据结构
void afterNodeInsertion(boolean evict) {
LinkedHashMap.Entry<K,V> first;
//如果顺序链表的头部节点不为null,则删除头部节点
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
//removeNode方法时HashMap的方法
removeNode(hash(key), key, null, false, true);
}
}
//2,LinkedHashMap 预留出来的扩展方法,如果此方法返回true,则在新增节点成功后删除顺序链表上的头部节点,也就是使用最少的节点,这个方法便是实现 用LinkedHashMap做缓存实现LRU算法的核心,默认返回的是false
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
//3,伪代码
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
if(容量 < 10){
return true;
}
return false;
}
接下来就是当数据修改后,LinkedHashMap的扩展方法afterNodeAccess,通过源码可以,每当每次put操作是更新了数据后,便把这个数据所在的节点放在顺序链的尾部,由此可知,调用频繁的节点总是处于链表的尾部,而链表的首部便是使用较少的甚至不用的数据,便可以删除.
void afterNodeAccess(Node<K,V> e) { // move node to last
//链表上的尾节点
LinkedHashMap.Entry<K,V> last;
//accessOrder的功能在这里编就体现了出来
if (accessOrder && (last = tail) != e) {
//拿到当前节点的上一个节点b和下一个节点a
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
//因为要将当前的节点放在链表的尾部,所以当前节点的after置为null
p.after = null;
//如果b为空,说明当前节点是链表的head,则设置当前节点的下一个节点为head,否则将当前节点的上一个节点和下一个节点连接起来
if (b == null)
head = a;
else
b.after = a;
//如果a不为null,则将a和b连接起来,如果a为null,则说明当前节点可能是尾节点或者链表为null,
//判断链表是不是为null是由b节点判断的,如果b为null,说明当前链表为null(a为null,b为null)
if (a != null)
a.before = b;
else
last = b;
//如果last为null,则说明链表为null,设置当前节点为链表的head,否则设置当前节点到链表的尾部
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
//更新尾节点
tail = p;
//更新操作数,作用于Fast-Fail
++modCount;
}
}
最后一个,当节点删除时,LinkedHashMap的扩展方法afterNodeRemoval。这个就比较简单了,就是删除当前节点,将当前节点的上一个和下一个节点相连
void afterNodeRemoval(Node<K,V> e) { // unlink
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.before = p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a == null)
tail = b;
else
a.before = b;
}
3,获取方法get()
LinkedHashMap重写了HashMap的get方法,在HashMap的get方法的原先逻辑上添加了排序逻辑,前面介绍的属性accessOrder在这里便有了用处,有源码可知,数据不光是在修改时会将当前节点移动到尾部,在获取时也会,但是去觉得accessOrder。
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
//如果允许排序
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
4,迭代器方法
我们猜测一下,如果使用迭代器遍历LinkedHashMap,如果采用HashMap的迭代器能按照顺序遍历出来吗?结果是否定的,HashMap并不知到LinkedHashMap内部双向链表的存在,所以LinkedHashMap也肯定重写了HashMap的迭代器逻辑
abstract class LinkedHashIterator {
LinkedHashMap.Entry<K,V> next;
LinkedHashMap.Entry<K,V> current;
//预期的map结构变化次数,用来判断是否抛出Fast-Fail 的异常
int expectedModCount;
LinkedHashIterator() {
//next等于链表的头部节点,遍历按照链表顺序遍历,HashMap next初始是等于null的
next = head;
expectedModCount = modCount;
current = null;
}
//和HashMap相同
public final boolean hasNext() {
return next != null;
}
//此处遍历是按照LinkedHashMap内部的双向链表来遍历,所以可以保证顺序
final LinkedHashMap.Entry<K,V> nextNode() {
LinkedHashMap.Entry<K,V> e = next;
//这里便是Map的Fast-Fail机制,如果你在迭代器中直接调用map的remove方法时,再次获取数据时便会报这个错,所以这也是为什么阿里巴巴java开发代码规范里,精致foreach时使用map删除方法的原因
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
current = e;
next = e.after;
return e;
}
//移除节点,如果在foreach中移除数据,应该采用迭代器的移除方法,因为迭代器中的移除方法在移除节点后将用于判断是否触发Fast-Fail的参数expectedModCount修改成LinkedHashMapmodCount
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount;
}
}
三,总结
LinkedHashMap源码其实挺简单,无非就是内部多了一条链表来记录节点的先后顺序,另外就是重写了一些HashMap的部分方法,给这些方法中添加了内部链表相关的逻辑,另外,虽然说LinkedHashMap可以做缓存,也支持LRU,但是基本上如果要使用缓存还是采用类似与redis中这种,专业的东西做专业的事。另外就是Fast-Fail机制也作用于list和set等Collections的子集,我们需要在工作中注意。
Map随笔:有序的HashMap——LinkedHashMap的更多相关文章
- Linkedlist,arrayDeque,HashMap,linkedHashMap
Linkedlist 1.extneds AbstractSequentialList, implements List<E>, Deque<E>, Cloneable, ja ...
- Map随笔:最常用的Map——HashMap
目录 Map随笔:最常用的Map--HashMap 前言: 1,HashMap的结构 2,HashMap的一些属性(JDK8) 3,HashMap的构造函数(JDK8) 4,HashMap的一些方法( ...
- Java 数据类型:集合接口Map:HashTable;HashMap;IdentityHashMap;LinkedHashMap;Properties类读取配置文件;SortedMap接口和TreeMap实现类:【线程安全的ConcurrentHashMap】
Map集合java.util.Map Map用于保存具有映射关系的数据,因此Map集合里保存着两个值,一个是用于保存Map里的key,另外一组值用于保存Map里的value.key和value都可以是 ...
- java HashMap,LinkedHashMap,TreeMap应用
共同点: HashMap,LinkedHashMap,TreeMap都属于Map:Map 主要用于存储键(key)值(value)对,根据键得到值,因此键不允许键重复,但允许值重复. 不同点: 1.H ...
- Java中List,ArrayList、Vector,map,HashTable,HashMap区别用法
Java中List,ArrayList.Vector,map,HashTable,HashMap区别用法 标签: vectorhashmaplistjavaiteratorinteger ArrayL ...
- HashMap LinkedHashMap源码分析笔记
MapClassDiagram
- Java中HashMap,LinkedHashMap,TreeMap的区别[转]
原文:http://blog.csdn.net/xiyuan1999/article/details/6198394 java为数据结构中的映射定义了一个接口java.util.Map;它有四个实现类 ...
- HashMap,LinkedHashMap,TreeMap对比
共同点: HashMap,LinkedHashMap,TreeMap都属于Map:Map 主要用于存储键(key)值(value)对,根据键得到值,因此键不允许键重复,但允许值重复. 不同点: 1.H ...
- Java容器类List、ArrayList、Vector及map、HashTable、HashMap的区别与用法
Java容器类List.ArrayList.Vector及map.HashTable.HashMap的区别与用法 ArrayList 和Vector是采用数组方式存储数据,此数组元素数大于实际存储的数 ...
随机推荐
- 探究 Nginx 中 reload 流程的真相
今天这篇文章主要来介绍下 Nginx 的 reload 流程.实际上在之前文章中,在更改了 nginx 配置文件时,我们都会执行 nginx -s reload 命令,我们执行这条命令的原因是希望 n ...
- Redis sorted set 常用命令介绍
Redis sorted set 使用: Redis 有序集合(sorted set) Redis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员. 不同的是每个元素都会关联一个 ...
- 大数据学习笔记——Hadoop高可用完全分布式模式完整部署教程(包含zookeeper)
高可用模式下的Hadoop集群搭建 本篇博客将会在之前写过的Linux的完整部署的基础上进行,暂时不会涉及到伪分布式或者完全分布式模式搭建,由于HA模式涉及到的配置文件较多,维护起来也较为复杂,相信学 ...
- Vue项目中实现用户登录及token验证
学习博客:https://www.cnblogs.com/web-record/p/9876916.html
- 使用for语句输出1-100之间的所有偶数
使用for语句输出1-100之间的所有偶数 for i in range(1,101): if i%2==0: print(i) 使用while语句输出1-100之间能够被3整除的数字 j=1 whi ...
- Java 从入门到进阶之路(十五)
在之前的文章我们介绍了一下 Java 中的接口,本章我们来看一下 Java 中类的多态. 在日常生活中,很多意思并不是我们想要的意思,如下: 1.领导:“你这是什么意思?” 小明:“没什么意思,意思意 ...
- VUEday01
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- 这几天加班熬夜把所有Python库整理了一遍,非常全面!
库名称简介 Chardet 字符编码探测器,可以自动检测文本.网页.xml的编码.colorama 主要用来给文本添加各种颜色,并且非常简单易用.Prettytable 主要用于在终端或浏览器端构建格 ...
- 【高可用架构】用Nginx实现负载均衡(三)
前言 在上一篇,已经用Envoy工具统一发布了Deploy项目代码.本篇我们来看看如何用nginx实现负载均衡 负载均衡器IP 192.168.10.11 [高可用架构]系列链接:待部署的架构介绍 演 ...
- 搭建zabbix 4.0
[root@localhost /]# sed ‐i "s#SELINUX=enforcing#SELINUX=disabled#g" /etc/selinux/config #永 ...