继承自HashMap,需要先查看"HashMap"一文先了解

要点总结

1.LinkHaspMap继承自HashMap,除了HashMap遍历无序的外,其余特性,比如扩容机制、key和value允许为Null等都是和HashMap一致;

2.LinkHaspMap内部额外维护了一个双向链表,在每次增删改查时,会增加或者调整链表的节点顺序,用于迭代时输出的顺序

3.默认遍历时是按照插入节点的顺序,而HashMap是无序的,这是2者最大的区别

4.LinkHashMap在实现时,就重写了几个方法以满足输出序列有序的需求

5.实现了HashMap中以下的空函数,用于维护LinkHashMap中的双向链表中节点顺序:

// Callbacks to allow LinkedHashMap post-actionsvoid afterNodeAccess(Node<K,V> p) { }void afterNodeInsertion(boolean evict) { }void afterNodeRemoval(Node<K,V> p) { }
复制代码

6.构造函数中增加支持设置accessOrder属性用于决定遍历时是按照插入节点的顺序输出还是访问的顺序输出,默认不设置为false,即按照插入节点的顺序输出

LinkedHashMap<String,String> linkHashMap =new LinkedHashMap<String,String>(16, (float) 0.75,true);linkHashMap.put("1","111");linkHashMap.put("2","222");linkHashMap.put("3","333");//原本顺序1,2,3//执行get完成后,顺序为 2,3,1linkHashMap.get("1");//执行get完成后,顺序为3,1,2linkHashMap.get("2");//执行get完成后,顺序为3,1,2,4,nulllinkHashMap.put("4","444");linkHashMap.put(null,null);Set<Map.Entry<String,String>> set=linkHashMap.entrySet();Iterator<Map.Entry<String,String>> iterator =set.iterator();while (iterator.hasNext()){Map.Entry<String,String> node=iterator.next();Log.d("tag",node.getKey()+","+node.getValue());}输出顺序为:accessOrder = true时3,3331,1112,2224,444null,nullaccessOrder = false时   1,1112,2223,3334,444null,null
复制代码

源码分析

构造函数

LinkHashMap构造函数的实现与HashMap基本相同,支持设置初始容量与负载因子,其中增加了一个参数accessOrder,用于决定遍历时是按照插入节点的顺序输出还是访问的顺序输出;

/*** The head (eldest) of the doubly linked list.*/
transient LinkedHashMapEntry<K,V> head;             //双向链表的头部元素(第一个节点)/*** The tail (youngest) of the doubly linked list.   //双向链表的尾部元素(最后一个节点)*/
transient LinkedHashMapEntry<K,V> tail;
/*** The iteration ordering method for this linked hash map: <tt>true</tt>* for access-order, <tt>false</tt> for insertion-order.** @serial*/
final boolean accessOrder; //决定遍历时的输出顺序,默认false为按插入顺序,否则按访问顺序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;}
复制代码

在HashMap节点转化为Node对象进行保存,而LinkHashMap则使用了继承自HashMap.Node的LinkedHashMapEntry对象来保存元素信息:

static class LinkedHashMapEntry<K,V> extends HashMap.Node<K,V> {LinkedHashMapEntry<K,V> before, after;LinkedHashMapEntry(int hash, K key, V value, Node<K,V> next) {super(hash, key, value, next);}}
复制代码

与HashMap.Node相比,增加了before、after用于表示节点的前一节点和后一节点,用于形成双向链表; 每次元素节点顺序发生改变时都会重新判断并赋值head、tail参数;

增加、修改

LinkHashMap的并没有重写继承自HashMap的put、putIfAbsent、replace、putAll这些增加修改元素的方法; 从HashMap一文中可以知道,最终都会直接putVal方法,如果只是修改原节点值,则进行value的修改,如果需要增加节点的话,则调用newNode用来创建新的节点,LinkHashMap重写了newNode方法,以及afterNodeInsertion方法

    //创建一个新的节点,重写了HashMap的newNode方法Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {LinkedHashMapEntry<K,V> p =new LinkedHashMapEntry<K,V>(hash, key, value, e);linkNodeLast(p);return p;}//维护更新双向链表// link at the end of list , 更新双向列表的头、尾节点private void linkNodeLast(LinkedHashMapEntry<K,V> p) {LinkedHashMapEntry<K,V> last = tail;  //获取当前尾节点tail = p;                             //尾节点重新赋值为新增加的节点pif (last == null) //如果原本不存在尾节点,说明p是唯一一个元素,头节点也赋值为phead = p;else {            //如果原本LinkHashMap不为空    p.before = last;    //将新节点p.before指向lastlast.after = p;     //将last.after指向p,更新了双向链表}}//新节点插入之后回调 , 根据evict 和   判断是否需要删除最老插入的节点。如果实现LruCache会用到这个方法。void afterNodeInsertion(boolean evict) { // possibly remove eldestLinkedHashMapEntry<K,V> first;if (evict && (first = head) != null && removeEldestEntry(first)) {K key = first.key;removeNode(hash(key), key, null, false, true);}}//LinkedHashMap 默认返回false 则不删除节点。 返回true 代表要删除最早的节点。通常构建一个LruCache会在达到Cache的上限是返回trueprotected boolean removeEldestEntry(Map.Entry<K,V> eldest) {return false;}
复制代码

删除

LinkHaspMap没有重写继承自HashMap的remove(key)、remove(key,value)方法,但是有重写了clear()方法、与afterNodeRemoval(Node<K,V> e)方法

也是简单调用super.clear()即HashMap的clear进行clear,并将自身维护的双向链表的头尾节点引用置空
public void clear() {super.clear();head = tail = null;}
复制代码
前面HashMap一文分析过,remove节点成功时,会执行afterNodeRemoval,只是HashMap默认是空实现
而LinkHashMap重写实现该方法,用于更新维护双向链表的头尾节点void afterNodeRemoval(Node<K,V> e) { // unlink//获取要移除节点e的前节点b与后节点aLinkedHashMapEntry<K,V> p =(LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;p.before = p.after = null;//如果前节点为空,说明要移除的是双向链表中的第一个节点,则重新对双向链表head头节点赋值为aif (b == null)head = a;else//否则,将要移除的节点e的前节点b与后节点a相连,形成新链表, b->ab.after = a;//如果后节点a为空,则说明要移除的是双向链表的最后一个节点,则重新对双向链表tail尾节点赋值为bif (a == null)tail = b;else//否则,将要移除的节点e的后节点a与前节点b相连,形成新链表, a->b ,双向,所以a、b互相指向a.before = b;}
复制代码

查询

LinkHashMap没有重写继承自HashMap的containsKey方法,但是有重写了get(k)、getOrDefault(K key,V defaultValue)、containsValue方法与afterNodeAccess(Node<K,V> e)方法;

//根据key获取节点valuepublic V get(Object key) {Node<K,V> e;if ((e = getNode(hash(key), key)) == null)return null;//LinkHashMap相比HashMap多个这个判断;如果设置了遍历需要根据访问顺序输出时,回调afterNodeAccess;if (accessOrder)  afterNodeAccess(e);return e.value;}//根据key获取节点value,若节点不存在则返回 defaultValuepublic V getOrDefault(Object key, V defaultValue) {Node<K,V> e;if ((e = getNode(hash(key), key)) == null)return defaultValue;//LinkHashMap相比HashMap多个这个判断;如果设置了遍历需要根据访问顺序输出时,回调afterNodeAccess;if (accessOrder) afterNodeAccess(e);return e.value;}//判断是否存在指定value的节点元素
//这边直接从LinkHashMap维护的双向链表进行遍历,相比HashMap的双重for循环可以提高一定效率(因为哈希表的实际容量比存放的元素节点大,这边需要遍历整个哈希表的容量个数,而双链表的节点个数即为实际存放的元素个数)
//而HashMap可以根据key的hash值计算出索引只需要调用getNode(int hash, Object key)单个for循环判断该位置的单向链表就行,如果全部数据遍历反而效率更低;public boolean containsValue(Object value) {for (LinkedHashMapEntry<K,V> e = head; e != null; e = e.after) {V v = e.value;if (v == value || (value != null && value.equals(v)))return true;}return false;}//LinkHashMap实现了该方法用于数据访问时,将访问的节点e移动到双向链表的末尾
//replace、get、getOrDefault以及通过put覆盖旧节点(注意是覆盖旧节点,如果是增加新节点回调的是afterNodeInsertion)会回调afterNodeAccessvoid afterNodeAccess(Node<K,V> e) { // move node to lastLinkedHashMapEntry<K,V> last;
//accessOrder参数的判断,以及获取尾节点last,判断e不是尾节点if (accessOrder && (last = tail) != e) {LinkedHashMapEntry<K,V> p =(LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
//p即将移动到尾节点,所以p.afer需要为nullp.after = null;
//如果b为null,则说明p原本是头节点,则将head重新执行p的下节点aif (b == null)head = a;
//否则将b下节点指向a                elseb.after = a;
//a不为null,将a前节点指向b                if (a != null)a.before = b;
//若原本p的后置节点a为null,则p原本就是尾节点了,更新last指向p的前置节点b       elselast = b;if (last == null)//原本尾节点为null,则说明链表中就一个节点head = p;else {//否则将p移动的双向链表的尾部p.before = last;last.after = p;}
//尾节点的引用赋值成p            tail = p;
//修改modCount++modCount;}}
复制代码

值得注意的是,afterNodeAccess()函数中,会修改modCount,因此当你正在accessOrder=true的模式下,迭代LinkedHashMap时,如果同时查询访问数据,也会导致fail-fast(即抛出抛出Concurrent Modification Exception),因为迭代的顺序已经改变。查看下面遍历的源码即可明白

遍历

以如下遍历为例:

    LinkedHashMap<String,String> linkHashMap =new LinkedHashMap<String,String>();linkHashMap.put("1","111");linkHashMap.put("2","222");linkHashMap.put("3","333");Set<Map.Entry<String,String>> set=linkHashMap.entrySet();Iterator<Map.Entry<String,String>> iterator =set.iterator();while (iterator.hasNext()){Map.Entry<String,String> node=iterator.next();Log.d("tag",node.getKey()+","+node.getValue());}
复制代码
transient Set<Map.Entry<K,V>> entrySet;//继承自HashMap的全局变量public Set<Map.Entry<K,V>> entrySet() {Set<Map.Entry<K,V>> es;//第一次调用=null,执行并返回new LinkedEntrySet()return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;}final class LinkedEntrySet extends AbstractSet<Map.Entry<K,V>> {//...//获取迭代器iteratorpublic final Iterator<Map.Entry<K,V>> iterator() {return new LinkedEntryIterator();}}//继承自LinkedHashIterator,重写了iterator.next()方法-------->调用nextNode()final class LinkedEntryIterator extends LinkedHashIteratorimplements Iterator<Map.Entry<K,V>> {public final Map.Entry<K,V> next() { return nextNode(); }}//linkHashMap.keySet().iterator()//继承自LinkedHashIterator,重写了iterator.next()方法-------->调用nextNode()final class LinkedKeyIterator extends LinkedHashIteratorimplements Iterator<K> {public final K next() { return nextNode().getKey(); }}//linkHashMap.values().iterator()//继承自LinkedHashIterator,重写了iterator.next()方法-------->调用nextNode()final class LinkedValueIterator extends LinkedHashIteratorimplements Iterator<V> {public final V next() { return nextNode().value; }}//实际Iteratorabstract class LinkedHashIterator {LinkedHashMapEntry<K,V> next;LinkedHashMapEntry<K,V> current;int expectedModCount;//构造函数,创建iterator实例的时候,获取双向链表头节点head,赋值expectedModCount= modCount//后续如果判断expectedModCount!=modCount,则代表变量过程元素发生了改变而引起fail-fast错误LinkedHashIterator() {next = head;expectedModCount = modCount;current = null;}//判断是否有下一节点public final boolean hasNext() {return next != null;}//获取下一个节点元素//抛出Concurrent Modification Exception情况//1.这边如果在遍历过程,改变哈希表元素数量的话,会导致modCount改变,从而modCount != expectedModCount//2.在accessOrder=true情况下,如果在遍历过程,异步get、replace操作导致触发afterNodeAccess而修改到modCount,也会致使modCount != expectedModCountfinal LinkedHashMapEntry<K,V> nextNode() {LinkedHashMapEntry<K,V> e = next;if (modCount != expectedModCount) //抛出Concurrent Modification Exceptionthrow new ConcurrentModificationException();if (e == null)throw new NoSuchElementException();current = e;next = e.after; //重新对next赋值下一节点,使得hasNext()一直可以正常工作return e;}//移除当前节点,抛出ConcurrentModificationException情况同上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);//removeNode()操作本身会修改modCount,所以这边重新对expectedModCount进行赋值expectedModCount = modCount;}}
复制代码

总结

1.LinkHashMap继承自HashMap,做了部分小改动使得其可以支持有序输出的特点: .内部维护了一个双向链表,迭代输出的顺序为双向链表头部到尾部各个节点的顺序 .每次增、删、改、查可能会触发调整双向链表的节点顺序

2.LinkHashMap还重写了containValue,使用遍历双向链表的单个for循环实现功能,相比HashMap需要整个遍历数组以及数组上每个索引位置的单向链表来得效率高; 因为HashMap数组的容量基本上比实际的有效元素节点数量大;

3.LinkHashMap的节点对象使用继承自HashMap.Node的LinkedHashMapEntry来描述,简单修改,支持双向

4.LinkHashMap没有重新put方法,但重新写了newNode()用于创建新节点,并将新节点链接到双向链表的尾部

5.从迭代器中可以知道抛出Concurrent Modification Exception的情况为:

.默认情况下(accessOrder=false)进行迭代输出,如果不通过iterator的remove()删除遍历到的节点,而是直接使用hashMap.remove、put等的话.accessOrder=true的情况下除了上诉的情况,还有在遍历过程中,如果还有其它线程在访问节点元素(get、remove),很容易造成fail-fast错误,即抛出Concurrent Modification Exception,模拟如下:.当然迭代遍历到最后一个元素节点后,因操作而引起modcout发生变化,因为要退出循环了,所以不会fail-fast
复制代码
accessOrder=true的情况下(相对少见),一般使用场景下,accessOrder默认为falsefinal LinkedHashMap<String,String> linkHashMap =new LinkedHashMap<String,String>(16, (float) 0.75,true);linkHashMap.put("1","111");linkHashMap.put("2","222");linkHashMap.put("3","333");linkHashMap.put("4","444");linkHashMap.put(null,null);Set<Map.Entry<String,String>> set=linkHashMap.entrySet();final Iterator<Map.Entry<String,String>> iterator =set.iterator();new Thread(new Runnable() {@Overridepublic void run() {while (iterator.hasNext()){//linkHashMap.get("1"); 一样会导致fail-fastMap.Entry<String,String> node=iterator.next();try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}//if(node.getKey()==null){//不会导致fail-fast,因为在最后一个遍历的节点访问,退出循环//    linkHashMap.get("1");// }Log.d("tag",node.getKey()+","+node.getValue());}}}).start();new Thread(new Runnable() {@Overridepublic void run() {linkHashMap.get("1");Log.d("tag","模拟其它线程访问");}}).start();输出:模拟其它线程访问3,333java.util.ConcurrentModificationExceptionat java.util.LinkedHashMap$LinkedHashIterator.nextEntry(LinkedHashMap.java:346)at java.util.LinkedHashMap$EntryIterator.next(LinkedHashMap.java:375)at java.util.LinkedHashMap$EntryIterator.next(LinkedHashMap.java:375)at com.yjz.performance.demo.MainActivity$1.run(MainActivity.java:87)at java.lang.Thread.run(Thread.java:818)
复制代码

扫描关注微信公众号:

Java集合(二、LinkHashMap)相关推荐

  1. Java集合(二):List列表

    在上一节中,介绍了Java集合的整体情况.从这节开始,将介绍具体的类.这里不单单介绍类的用法,还会试图从源码的角度分析类的实现.这一节将介绍List接口及实现类,即列表中的链表LinkedList和数 ...

  2. 由浅入深理解java集合(二)——集合 Set

    2019独角兽企业重金招聘Python工程师标准>>> 一.HashSet类 HashSet是Set接口的典型实现,实现了Set接口中的所有方法,并没有添加额外的方法,大多数时候使用 ...

  3. java基础—TreeSet集合中储存自定义对象(java集合二)

    TreeSet集合中储存学生对象,按照其年龄进行排序 TreeSet对元素进行排序的方式一: 让元素自身具备比较功能,元素就需要实现Comparable接口,覆盖compareTo方法. TreeSe ...

  4. Java集合(二二): LinkedList源码剖析

    目录 1.LinkedList简介 2.LinkedList数据结构 3.ArrayList源码分析LinkedList3.ArrayList源码分析 3.1 ArrayList继承结构和层次关系Li ...

  5. Java集合(二)-Set集合

    来源:流浪舟 https://www.maliaoblog.cn/2020/1003 公众号: 菜鸡干Java 文章目录 HashSet类 重写hashCode方法步骤 LinkedHashSet T ...

  6. JAVA集合二:HashMap和Hashtable

    参考链接: HOW2J.CN HashMap HashMap实现了JAVA的Map接口,类似于C++的STL框架的Map,是存储键值对的数据结构.键(key)是唯一的,但值(value)可以重复,如果 ...

  7. Java集合(一):Java集合概述

    注:本文基于JDK 1.7 1 概述 Java提供了一个丰富的集合框架,这个集合框架包含了许多接口.虚拟类和实现类.这些接口和类提供了丰富的功能,能够满足基本的聚合需求.下图就是这个框架的整体结构图: ...

  8. 三十二、Java集合中的ArrayList

    @Author:Runsen @Date:2020/6/3 作者介绍:Runsen目前大三下学期,专业化学工程与工艺,大学沉迷日语,Python, Java和一系列数据分析软件.导致翘课严重,专业排名 ...

  9. java学习记录十五:集合二Collections、Set、Map

    java学习记录十五:集合二 一.Collections工具类 一.解释 二.常用方法 1.打乱集合顺序 2.按照默认规则排序 3.按指定规则排序 4.批量添加元素 二.可变参数 一.解释 二.写法 ...

最新文章

  1. Python超越Java,Rust持续称王!Stack Overflow 2019开发者报告
  2. MSSql-1内部数据库版本号
  3. QT的QSignalTransition类的使用
  4. struts2中拦截器的使用
  5. oracle的标准写法,oracle 表连接特有写法与标准写法
  6. Thread-Per-Message设计模式
  7. java已知 求p_Java 0520 第五次课作业
  8. JVM之静态编译优化以及JIT编译
  9. Oracle开发艺术
  10. 每日一道剑指offer-从尾到头打印链表
  11. 日常生活中的思维导图:一本书让你了解思维导图MindManager
  12. Windows下安装JDK,配置环境变量
  13. 阅读圣经丨变量引起的上下文
  14. XP卸载IE8的方法
  15. 查看Windows系统的开机、关机时间
  16. 小丽和小张 --- 作者:歪瓜裂枣
  17. 计算机网络与互联网的区别,计算机网络与互联网的主要区别是什么?
  18. Google Scanned Objects: A High-Quality Dataset of 3D Scanned Household Items【google 3D数据集】
  19. pyscripter与python的关系_我用过的最好的python编辑器PyScripter
  20. YTU 问题 : 逆序数字排序

热门文章

  1. 灰度重采样的方法分为_DSA医疗影像增强技术特点及方法
  2. angular6 设置全局变量_Angularjs 设置全局变量的方法总结
  3. eureka动态扩容_SpringCloud- 第二篇 Eureka快速上手
  4. dis的前缀单词有哪些_英语单词II词根词缀记忆法:必背英语前缀DIS (DI, DIF)
  5. python解决xml文件中存在中文文字的问题
  6. iotop命令监控磁盘io
  7. spring boot +spring security + jwt 实现认证模块
  8. Linux系统下的权限详解-1
  9. shutdown immediate ,无法关闭数据库的解决方案
  10. BZOJ1877:[SDOI2009]晨跑——题解