简介

LinkedHashMap继承HashMap(线程不安全…),内部主体还是一个哈希表,底层用的仍然是拉链式散列结构,由数组+链表/红黑树组成(加入红黑树分析起来可能会有一点乱,本文淡化红黑树分析,以双向链表为主。如果对红黑树不熟悉,可以参考我的红黑树学习笔记)。在HashMap的基础上,重写了一个Entry,并在上面添加了两个变量:前继结点引用before和后继结点引用after,也就是双向链表。所以LinkedHashMap的内部结构和HashMap差不多,唯一就是升级为了双向链表,通过双向链表解决了HashMap不能保证遍历顺序和插入顺序的缺点。

看一下成员变量

accessOrder表示是否按照访问顺序排序,其中一个构造器可以指定accessOrder。既然是继承了HashMap,那么不用多说,也是线程不安全的…另外这里有个疑问,既然LinkedHashMap继承HashMap,HashMap实现了Map接口,那怎么LinkedHashMap还要再去显式的再去实现Map接口呢?个人猜测可能只是强调一下LinkedHashMap真的是实现Map接口,是Map的小弟,这里不实现也没啥毛病。

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V> {//双向链表头结点,遍历的时候能快速找到他private transient Entry<K,V> header;//是否按访问顺序排序private final boolean accessOrder;...
}

Entry节点

上面提到了Entry,是在HashMap的基础上重写了Entry并增加了before和after两个变量,使其拓展成了双向链表。详细的应该是这样的:entey<——before+entry(hash+key+value+next)+after——>entry。可能next和after全都指向下一个entry,也有可能next——>null,而after——>entry。

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);}
}

另外还有分别指向表头、表尾的两个变量:head和tail

//双向链表的头结点
transient LinkedHashMap.Entry<K,V> head;
//双向链表的尾节点
transient LinkedHashMap.Entry<K,V> tail;

构造方法

相比于HashMap,无非就是多了一个控制迭代的节点输出顺序accessOrder

1.空构造

默认accessOrder为false,即迭代时的输出顺序就是插入顺序。比如插入1、2、3,迭代输出就是1、2、3;若为true,输出顺序即节点访问顺序。比如插入1、2、3,迭代之前访问了2,又访问了1,迭代出来就是3、2、1

public LinkedHashMap() {super();accessOrder = false;}

2.指定初始化容量

public LinkedHashMap(int initialCapacity) {super(initialCapacity);accessOrder = false;}

3.指定初始化容量、加载因子

public LinkedHashMap(int initialCapacity, float loadFactor) {super(initialCapacity, loadFactor);accessOrder = false;}

4.指定初始化容量、加载因子、输出顺序变量

 public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {super(initialCapacity, loadFactor); this.accessOrder = accessOrder;
}

5.指定Map

map包浆变LinkedHashMap

public LinkedHashMap(Map<? extends K, ? extends V> m) {super(); accessOrder = false; //批量插入一个map中的所有数据到本集合中。 putMapEntries(m, false);
}

辅助方法putMapEntries

//evict初始化时为false,其他情况为truefinal void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {//拿到m的元素数量int s = m.size();//m数量大于0if (s > 0) {//当前表是空的if (table == null) { // pre-size//根据m的元素数量和当前表的加载因子,计算出阈值float ft = ((float)s / loadFactor) + 1.0F;//修正阈值的边界 不能超过MAXIMUM_CAPACITYint t = ((ft < (float)MAXIMUM_CAPACITY) ?(int)ft : MAXIMUM_CAPACITY);//如果新的阈值 > 当前阈值if (t > threshold)threshold = tableSizeFor(t);//>=新的阈值的 满足2的n次方的阈值}//如果不空且m的元素数量大于阈值else if (s > threshold)resize();// 一定得扩容了//遍历 m 依次将元素加入当前表中。for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {K key = e.getKey();V value = e.getValue();putVal(hash(key), key, value, false, evict);}}}

常规操作

1.put方法

在JDK1.8中并没有重写put方法,只是调用了putVal方法,并在该方法中调用了其他辅助方法。总的来说,LinkedHashMap重写了newNode(),在每次构建新节点时,通过linkNodeLast§;将新节点链接在内部双向链表的尾部

public V put(K key, V value) {return putVal(hash(key), key, value, false, true);
}

辅助方法putVal

该方法会在putMapEntries批量插入数据或者put插入单个数据时调用

// HashMap中的实现
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab; Node<K,V> p; int n, i;if ((tab = table) == null || (n = tab.length) == 0) {...}// 通过节点 hash 定位节点所在的桶位置,并检测桶中是否包含节点引用if ((p = tab[i = (n - 1) & hash]) == null) {...}else {Node<K,V> e; K k;if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;//TreeNode...else if (p instanceof TreeNode) {...}else {// 对链表遍历,并计算链表长度for (int binCount = 0; ; ++binCount) {// 未在单链表中找到要插入的节点,将新节点放在单链表的后面if ((e = p.next) == null) {//构建新节点,通过linkNodeLast(p)将新节点链接在内部双向链表的尾部p.next = newNode(hash, key, value, null);if (binCount >= TREEIFY_THRESHOLD - 1) {...}break;}// 插入的节点已经存在于单链表中if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))break;p = e;}}if (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null) {...}afterNodeAccess(e);    // 回调方法,后续说明return oldValue;}}++modCount;if (++size > threshold) {...}//这个方法我想就不用再多说了吧...HashMap专门留下来的,新节点插入之后回调afterNodeInsertion(evict);return null;
}

辅助方法newNode

把新节点接在双向链表的尾部

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);// 将 Entry 接在双向链表的尾部linkNodeLast(p);return p;
}

辅助方法linkNodeLast

将新增的节点,链接在链表尾部

private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {LinkedHashMap.Entry<K,V> last = tail;tail = p;// last 为 null,表明链表还未建立if (last == null)head = p;else {// 将新节点 p 接在链表尾部p.before = last;last.after = p;}
}

2.get方法

accessOrder为true的时候,要去回调afterNodeAccess(Node<K,V> e)方法

public V get(Object key) {Node<K,V> e;if ((e = getNode(hash(key), key)) == null)return null;// 如果 accessOrder 为 true,则调用 afterNodeAccess 将被访问节点移动到链表最后if (accessOrder)afterNodeAccess(e);return e.value;
}

辅助方法afterNodeAccess

将当前被访问到的节点e,移动至内部的双向链表的尾部

void afterNodeAccess(Node<K,V> e) {LinkedHashMap.Entry<K,V> last;//原来的尾结点//accessOrder 是true ,且原尾节点不等于eif (accessOrder && (last = tail) != e) {//e强转成双向链表节点pLinkedHashMap.Entry<K,V> p =(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;p.after = null;//p现在是尾节点, 后置节点一定是null//如果p的前置节点是null,则p以前是头结点,所以更新现在的头结点是p的后置节点aif (b == null)head = a;else //否则更新p的前直接点b的后置节点为ab.after = a;//如果p的后置节点不是null,则更新后置节点a的前置节点为bif (a != null)a.before = b;//如果原本p的后置节点是null,则p就是尾节点。 此时 更新last的引用为 p的前置节点belselast = b;//原本尾节点是null  则,链表中就一个节点if (last == null)head = p;else {  //否则 更新 当前节点p的前置节点为 原尾节点last, last的后置节点是p// 将 p 接在链表的最后p.before = last;last.after = p;}tail = p;//尾节点的引用赋值成p++modCount;}
}

3.remove方法

因为LinkedHashMap的删除逻辑和HashMap无异,所以并没有重写remove方法。但是重写了afterNodeRemoval方法,会在removeNode被调用。值得注意的是,removeNode方法会在所有和删除结点的方法中被调用,是整个remove事件中的“刽子手”。概括一下整个过程,有3板斧:1.根据hash值定位到桶的位置;2.分情况遍历链表或调用红黑树remove方法;3.将目标从LinkedHashMap的双向链表中移除(当然往后节点的前后指向关系就变了)

public V remove(Object key) {Node<K,V> e;return (e = removeNode(hash(key), key, null, false, true)) == null ?null : e.value;
}
//删除节点
final Node<K,V> removeNode(int hash, Object key, Object value,boolean matchValue, boolean movable) {Node<K,V>[] tab; Node<K,V> p; int n, index;// p 是待删除节点的前置节点//hash表不为空,根据hash值算出来的index位置下有结点if ((tab = table) != null && (n = tab.length) > 0 &&(p = tab[index = (n - 1) & hash]) != null) {Node<K,V> node = null, e; K k; V v;//node是待删除结点if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))node = p;//将待删除节点引用赋给nodeelse if ((e = p.next) != null) { //循环遍历 找到待删除节点,赋值给nodeif (p instanceof TreeNode) {...}//暂时先淡化这一部分else {// 遍历单链表,寻找要删除的节点,并赋值给 node 变量do {if (e.hash == hash &&((k = e.key) == key ||(key != null && key.equals(k)))) {node = e;break;}p = e;} while ((e = e.next) != null);}}//如果有待删除节点node,  且 matchValue为false,或者值也相等if (node != null && (!matchValue || (v = node.value) == value ||(value != null && value.equals(v)))) {if (node instanceof TreeNode) {...}// 将要删除的节点从单链表中移除else if (node == p)//说明是链表头是待删除节点tab[index] = node.next;else//待删除节点在表中间p.next = node.next;++modCount;--size;afterNodeRemoval(node);    // 调用删除回调方法进行后续操作return node;}}return null;
}

辅助方法afterNodeRemoval

删除节点e的同时将e从双向链表上删除

void afterNodeRemoval(Node<K,V> e) { // unlinkLinkedHashMap.Entry<K,V> p =(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;//待删除节点 p 的前置后置节点都置空p.before = p.after = null;//如果前置节点是null,则现在的头结点应该是后置节点aif (b == null)head = a;//否则将前置节点b的后置节点指向aelseb.after = a;//同理如果后置节点时null ,则尾节点应是bif (a == null)tail = b;//否则更新后置节点a的前置节点为belsea.before = b;
}

最后

LinkedHashMap继承了HashMap,只是重写了几个方法,以及在构造中加入了影响迭代时输出顺序的变量。只要增加、修改、删除节点,就会增加节点或者修改链表的节点顺序。
accessOrder这个变量十分的关键,起着决定性作用。false,插入节点的顺序;true,访问节点的顺序。
put并没有重写,只是重写了newNode方法,并通过一系列辅助方法来把新构建的结点链接到双向链表的尾部。

LinkedHashMap源码分析相关推荐

  1. Java类集框架 —— LinkedHashMap源码分析

    前言 我们知道HashMap底层是采用数组+单向线性链表/红黑树来实现的,HashMap在扩容或者链表与红黑树转换过程时可能会改变元素的位置和顺序.如果需要保存元素存入或访问的先后顺序,那就需要采用L ...

  2. 散列表、LinkedHashMap源码分析

    一.散列表 1.散列思想 散列表用的是数组支持按照下标随机访问数据的时候,时间复杂度是O(1)O(1)O(1)的特性.通过散列函数把元素的键值映射为下标,然后把数据存储在数组中对应下标的位置.当按照键 ...

  3. Java集合之LinkedHashMap源码分析

    概述 HashMap是无序的, 即put的顺序与遍历顺序不保证一样. LinkedHashMap是HashMap的一个子类, 它通过重写父类的相关方法, 实现自己的功能. 它保留插入的顺序. 如果需要 ...

  4. 【Java源码分析】LinkedHashMap源码分析

    类的定义 public class LinkedHashMap<K, V> extends HashMap<K, V> {} 基于双向链表实现,属于Map的一类,其父类是Has ...

  5. HashSet及LinkedHashSet源码分析(基于JDK1.6)

    Java容器类的用途是"保存对象",分为两类:Map--存储"键值对"组成的对象:Collection--存储独立元素.Collection又可以分为List和 ...

  6. LinkedHashMap 源码详细分析(JDK1.8)

    1. 概述 LinkedHashMap 继承自 HashMap,在 HashMap 基础上,通过维护一条双向链表,解决了 HashMap 不能随时保持遍历顺序和插入顺序一致的问题.除此之外,Linke ...

  7. Java Collection系列之HashMap、ConcurrentHashMap、LinkedHashMap的使用及源码分析

    文章目录 HashMap HashMap的存储结构 初始化 put & get put元素 get元素 扩容 遍历Map jdk1.8中的优化 ConcurrentHashMap jdk1.7 ...

  8. 基于JDK1.8---HashMap源码分析

    基于JDK1.8-HashMap源码简要分析 HashMap继承关系 HashMap:根据键的 hashCode 值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不 ...

  9. java.util.ServiceLoader源码分析

    java.util.ServiceLoader源码分析 回顾: ServiceLoader类的使用(具体参考博客http://blog.csdn.net/liangyihuai/article/det ...

最新文章

  1. 钉钉api 获取 accesstoken_低代码快速对接钉钉日程
  2. 深思:外卖背后的人工智能算法揭秘
  3. python小游戏编程实例-10分钟教你用Python写一个贪吃蛇小游戏,适合练手项目
  4. mysql syncbinlog_Mysql之sync-binlog参数
  5. Java培训学习笔记分享:SpringMVC框架
  6. WCF HelpPage 和自动根据头返回JSON XML
  7. 保险科技服务商豆包网完成9500万新一轮融资,博将资本领投
  8. 算法(algorithm):#include<algorithm>
  9. python基础:购物车代码
  10. 《数字信号处理》——(一).DTFT、DFT(python实现)
  11. 项目成本管理的5项原则
  12. php 接口文档写法,php 接口文档
  13. 7-14 输出大写英文字母 (15分) 瞎搞
  14. 金蝶KIS标准版会计期间超过三期。。。
  15. 学渣的刷题之旅 leetcode刷题 66. 加一
  16. Android学习|控件——Notification通知
  17. 城市货车通行码二维码解析
  18. 输入一个字符,一个数字,一个单精度浮点数,一个双精度浮点数,按顺序输出它们四个 且数字指定占4个字符宽靠右对齐,单精度浮点数保留2位小数,双精度保留12位小数,占一行输出、空格分隔
  19. 猪圈密码(Pigpen)
  20. 基岩版服务器映射,Sakura Frp 客户端使用教程 (Minecraft 服务端映射示例)

热门文章

  1. windows环境下搭建rabbitMQ开发环境
  2. mongo explain分析详解
  3. ios中一个开发者证书如何创建多个app应用
  4. 精确到秒的JQuery日期控件,jquery日历插件,jquery日期插件
  5. 国内国外虚拟主机的对比
  6. Shell之sed用法 转滴
  7. 手动安装oracle软件 删软件
  8. ​rsync应用拓展多模块同步13
  9. 连接到localhost后提示要求用户名和密码
  10. Nginx之rewrite使用