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的更多相关文章

  1. Linkedlist,arrayDeque,HashMap,linkedHashMap

    Linkedlist 1.extneds AbstractSequentialList, implements List<E>, Deque<E>, Cloneable, ja ...

  2. Map随笔:最常用的Map——HashMap

    目录 Map随笔:最常用的Map--HashMap 前言: 1,HashMap的结构 2,HashMap的一些属性(JDK8) 3,HashMap的构造函数(JDK8) 4,HashMap的一些方法( ...

  3. Java 数据类型:集合接口Map:HashTable;HashMap;IdentityHashMap;LinkedHashMap;Properties类读取配置文件;SortedMap接口和TreeMap实现类:【线程安全的ConcurrentHashMap】

    Map集合java.util.Map Map用于保存具有映射关系的数据,因此Map集合里保存着两个值,一个是用于保存Map里的key,另外一组值用于保存Map里的value.key和value都可以是 ...

  4. java HashMap,LinkedHashMap,TreeMap应用

    共同点: HashMap,LinkedHashMap,TreeMap都属于Map:Map 主要用于存储键(key)值(value)对,根据键得到值,因此键不允许键重复,但允许值重复. 不同点: 1.H ...

  5. Java中List,ArrayList、Vector,map,HashTable,HashMap区别用法

    Java中List,ArrayList.Vector,map,HashTable,HashMap区别用法 标签: vectorhashmaplistjavaiteratorinteger ArrayL ...

  6. HashMap LinkedHashMap源码分析笔记

    MapClassDiagram

  7. Java中HashMap,LinkedHashMap,TreeMap的区别[转]

    原文:http://blog.csdn.net/xiyuan1999/article/details/6198394 java为数据结构中的映射定义了一个接口java.util.Map;它有四个实现类 ...

  8. HashMap,LinkedHashMap,TreeMap对比

    共同点: HashMap,LinkedHashMap,TreeMap都属于Map:Map 主要用于存储键(key)值(value)对,根据键得到值,因此键不允许键重复,但允许值重复. 不同点: 1.H ...

  9. Java容器类List、ArrayList、Vector及map、HashTable、HashMap的区别与用法

    Java容器类List.ArrayList.Vector及map.HashTable.HashMap的区别与用法 ArrayList 和Vector是采用数组方式存储数据,此数组元素数大于实际存储的数 ...

随机推荐

  1. linux—chmod

    chmod -options -c 只输出被改变的文件信息      -f , --silent, --quite   当chmod不能改变文件模式时,不通知用户      -R   递归       ...

  2. 【Vuejs】397- Vue 3最值得期待的五项重大更新

    作者|Filip Rakowski 译者|王强 编辑|王文婧 最近关于即将发布的 Vue.js 的第 3 个大版本的消息越来越密集.虽然本文所讨论的内容还没有完全确定下来,但作者已经可以肯定它将是对当 ...

  3. Rabbit安装(单机及集群,阿里云)

    Rabbit安装(单机及集群,阿里云) 前言 虽然我并不是部署人员,但是自己私人测试环境的各类东东还是得自己安装的. 尤其在规模不大的公司,基本安装部署工作都是后端的份内之事. 那么最令人痛苦的,莫过 ...

  4. 数组(Array)和列表(ArrayList)有什么区别?什么时候应该使用Array而不是ArrayList?

    下面列出了Array和ArrayList的不同点:Array可以包含基本类型和对象类型,ArrayList只能包含对象类型.Array大小是固定的,ArrayList的大小是动态变化的.ArrayLi ...

  5. IIS配置svc(IIS8中添加WCF支持几种方法小结)

    方法一 最近在做Silverlight,Windows Phone应用移植到Windows 8平台,在IIS8中测试一些传统WCF服务应用,发现IIS8不支持WCF服务svc请求,后来发现IIS8缺少 ...

  6. Winform中实现批量文件复制(附代码下载)

    场景 效果 将要批量复制的文件拖拽到窗体中,然后点击下边选择目标文件夹,然后点击复制按钮. 注: 博客主页: https://blog.csdn.net/badao_liumang_qizhi 关注公 ...

  7. Winform中实现拖拽文件到ListView获取文件类型(附代码下载)

    场景 效果 注: 博客主页: https://blog.csdn.net/badao_liumang_qizhi关注公众号 霸道的程序猿 获取编程相关电子书.教程推送与免费下载. 实现 新建一个for ...

  8. FlowPortal:流程节点定义有误,合流节点"合流"没有对应的聚焦节点

    FB版本:6.00c 报错: 流程节点定义有误,合流节点"合流"没有对应的聚焦节点 解决办法:分流和合流之间的节点不能有其他节点汇入.调整如下后,成功保存.

  9. DDMS files not found: xxx\hprof-conv.exe

    出现如下错误: DDMS files not found: xxx\hprof-conv.exe The connection to adb is down, and a severe error h ...

  10. Nginx核心流程及模块介绍

    Nginx核心流程及模块介绍 1. Nginx简介以及特点 Nginx简介: Nginx (engine x) 是一个高性能的web服务器和反向代理服务器,也是一个IMAP/POP3/SMTP服务器 ...