引言

关于java中的不常见模块,让我一下子想我也想不出来,所以我希望以后每次遇到的时候我就加一篇。上次有人建议我写全所有常用的Map,所以我研究了一晚上LinkedHashMap,把自己感悟到的解释给大家。在本篇博文中,我会用一个例子展现LinkedHashMap的运行和初始化情况,展示LinkedHashMap的数据存储情况,同时用JDK1.8中它的源代码解释给大家。其实,在以前的博文中我就已经粗略的介绍过了,关于缓存机制的实现:http://blog.csdn.net/u012403290/article/details/68926201中就涉及到了LinkedHashMap,不过今天我们会从源码解释它为什么可以实现LRU(最近最少使用)缓存。最后,笔者目前整理的一些blog针对面试都是超高频出现的。大家可以点击链接:http://blog.csdn.net/u012403290

注意

我希望看这篇的文章的小伙伴如果没有了解过HashMap那么可以先看看我这篇文章:http://blog.csdn.net/u012403290/article/details/65442646,在这篇文章中我详细介绍了HashMap的底层实现和一些常见的成员变量。只有在对HashMap有一定的了解之后,才能很好的理解LinkedHashMap,因为它是继承HashMap实现的。所以对于加载因子,容量,桶的概念就不再赘述。

数据存储结构

我们已经知道HashMap是以散列表的形式存储数据的,LinkedHashMap继承了HashMap,所以LinkedHashMap其实也是散列表的结构,但是“linked”是它对HashMap功能的进一步增强,LinkedHashMap用双向链表的结构,把所有存储在HashMap中的数据连接起来。有人会说散列表不是已经有了链表的存储结构了嘛,为什么还要来个双向链表?桶(桶的概念就是数组的一个存储节点,比如说arr[0]是一个桶,arr[1]也是一个桶)中的链表和这个双向链表是两个概念,以下是我总结的区别:①桶中的链表是散列表结构的一部分;而双向链表是LinkedHashMap的额外引入;②桶中的链表只做数据存储,没有存储顺序的概念;双向链表的核心就是控制数据存储顺序(存储顺序是LinkedHashMap的核心);③桶中的链表产生是因为发生了hash碰撞,导致数据散落在一个桶中,用链表给予存储,所以这个链表控制了一个桶;双向链表是要串连所有的数据,也就是说有桶中的数据都是会被这个双向链表管理。

所以,我修改了HashMap的图片,大家参考下: 
 
所以,简单来说就是LinkedHashMap相比于HashMap来说就是多了这些红色的双向链表而已。

两种演示

LinkedHashMap的核心就是存在存储顺序和可以实现LRU算法,所以下面我会用两个demo先来证明这两种情况: 
①、放入到LinkedHashMap是有顺序的,会按照你放入的顺序存储:

  1. package com.brickworkers;
  2. import java.util.LinkedHashMap;
  3. /**
  4. * @author Brickworker
  5. * Date:2017年4月12日下午12:46:25
  6. * 关于类LinkedHashMapTest.java的描述:jdk1.8逐字逐句带你理解linkedHashMap
  7. * Copyright (c) 2017, brcikworker All Rights Reserved.
  8. */
  9. public class LinkedHashMapTest {
  10. public static void main(String[] args) {
  11. LinkedHashMap<Integer, Integer> map = new LinkedHashMap<Integer, Integer>();
  12. for (int i = 0; i < 10; i++) {//按顺序放入1~9
  13. map.put(i, i);
  14. }
  15. System.out.println("原数据:"+map.toString());
  16. map.get(3);
  17. System.out.println("查询存在的某一个:"+map.toString());
  18. map.put(4, 4);
  19. System.out.println("插入已存在的某一个:"+map.toString()); //直接调用已存在的toString方法,不然自己需要用迭代器实现
  20. map.put(10, 10);
  21. System.out.println("插入一个原本没存在的:"+map.toString());
  22. }
  23. //输出结果
  24. // 原数据:{0=0, 1=1, 2=2, 3=3, 4=4, 5=5, 6=6, 7=7, 8=8, 9=9}
  25. // 查询存在的某一个:{0=0, 1=1, 2=2, 3=3, 4=4, 5=5, 6=6, 7=7, 8=8, 9=9}
  26. // 插入已存在的某一个:{0=0, 1=1, 2=2, 3=3, 4=4, 5=5, 6=6, 7=7, 8=8, 9=9}
  27. // 插入一个原本没存在的:{0=0, 1=1, 2=2, 3=3, 4=4, 5=5, 6=6, 7=7, 8=8, 9=9, 10=10}
  28. }

观察以上代码,其实它是符合先进先出的规则的,不管你怎么查询插入已存在的数据,不会对排序造成影响,如果有新插入的数据将会放在最尾部。

②了解,启用LRU规则的LinkedHashMap,启动这个规则需要在构造LinkedHashMap的时候,调用三个参数的构造器,这个构造器源码如下:

  1. /**
  2. * Constructs an empty <tt>LinkedHashMap</tt> instance with the
  3. * specified initial capacity, load factor and ordering mode.
  4. *
  5. * @param initialCapacity the initial capacity
  6. * @param loadFactor the load factor
  7. * @param accessOrder the ordering mode - <tt>true</tt> for
  8. * access-order, <tt>false</tt> for insertion-order
  9. * @throws IllegalArgumentException if the initial capacity is negative
  10. * or the load factor is nonpositive
  11. */
  12. public LinkedHashMap(int initialCapacity,
  13. float loadFactor,
  14. boolean accessOrder) {
  15. super(initialCapacity, loadFactor);
  16. this.accessOrder = accessOrder;//是否开启LRU规则
  17. }

第三个参数accessOrder就是用于控制LRU规则的。 
如下就是我写的demo:

  1. package com.brickworkers;
  2. import java.util.LinkedHashMap;
  3. /**
  4. * @author Brickworker
  5. * Date:2017年4月12日下午12:46:25
  6. * 关于类LinkedHashMapTest.java的描述:jdk1.8逐字逐句带你理解linkedHashMap
  7. * Copyright (c) 2017, brcikworker All Rights Reserved.
  8. */
  9. public class LinkedHashMapTest {
  10. public static void main(String[] args) {
  11. LinkedHashMap<Integer, Integer> map = new LinkedHashMap<Integer, Integer>(20, 0.75f, true);
  12. for (int i = 0; i < 10; i++) {//按顺序放入1~9
  13. map.put(i, i);
  14. }
  15. System.out.println("原数据:"+map.toString());
  16. map.get(3);
  17. System.out.println("查询存在的某一个:"+map.toString());
  18. map.put(4, 4);
  19. System.out.println("插入已存在的某一个:"+map.toString()); //直接调用已存在的toString方法,不然自己需要用迭代器实现
  20. map.put(10, 10);
  21. System.out.println("插入一个原本没存在的:"+map.toString());
  22. }
  23. //输出结果
  24. // 原数据:{0=0, 1=1, 2=2, 3=3, 4=4, 5=5, 6=6, 7=7, 8=8, 9=9}
  25. // 查询存在的某一个:{0=0, 1=1, 2=2, 4=4, 5=5, 6=6, 7=7, 8=8, 9=9, 3=3} //被访问(get)的3放到了最后面
  26. // 插入已存在的某一个:{0=0, 1=1, 2=2, 5=5, 6=6, 7=7, 8=8, 9=9, 3=3, 4=4}//被访问(put)的4放到了最后面
  27. // 插入一个原本没存在的:{0=0, 1=1, 2=2, 5=5, 6=6, 7=7, 8=8, 9=9, 3=3, 4=4, 10=10}//新增一个放到最后面
  28. }

从上面可以看出,每当我get或者put一个已存在的数据,就会把这个数据放到双向链表的尾部,put一个新的数据也会放到双向链表的尾部。

逐字逐句底层源码

接下来我们通过源码深入学习LinkedHash,同时解答上述出现的有趣的事情。

①分析LinkedHashMap的类名和继承关系:

  1. public class LinkedHashMap<K,V>
  2. extends HashMap<K,V>
  3. implements Map<K,V>
  4. {

从这里我们可以看出LinkedHashMap是继承了HashMap并实现了Map接口的,所以它和HashMap的关系肯定不一般。

②分析LinkedHashMap的构造函数:

  1. //1
  2. public LinkedHashMap(int initialCapacity, float loadFactor) {
  3. super(initialCapacity, loadFactor);
  4. accessOrder = false;
  5. }
  6. //2
  7. public LinkedHashMap(int initialCapacity) {
  8. super(initialCapacity);
  9. accessOrder = false;
  10. }
  11. //3
  12. public LinkedHashMap() {
  13. super();
  14. accessOrder = false;
  15. }
  16. //4
  17. public LinkedHashMap(Map<? extends K, ? extends V> m) {
  18. super();
  19. accessOrder = false;
  20. putMapEntries(m, false);
  21. }
  22. //5
  23. public LinkedHashMap(int initialCapacity,
  24. float loadFactor,
  25. boolean accessOrder) {
  26. super(initialCapacity, loadFactor);
  27. this.accessOrder = accessOrder;
  28. }

它具有5个构造函数,可以设置容量和加载因子,且在默认情况下是不开启LRU规则的。同时它还以用具有继承K,V关系的map进行初始化。

③分析双向链表

  1. /**
  2. * HashMap.Node subclass for normal LinkedHashMap entries.
  3. */
  4. static class Entry<K,V> extends HashMap.Node<K,V> {
  5. //前后指针
  6. Entry<K,V> before, after;
  7. Entry(int hash, K key, V value, Node<K,V> next) {
  8. super(hash, key, value, next);
  9. }
  10. }
  11. /**
  12. * The head (eldest) of the doubly linked list.
  13. */
  14. transient LinkedHashMap.Entry<K,V> head;//双向链表头节点(最老)
  15. /**
  16. * The tail (youngest) of the doubly linked list.
  17. */
  18. transient LinkedHashMap.Entry<K,V> tail;//双向列表尾节点(最新)

用一个静态内部类Entry表示双向链表,实现了HashMap中的Node内部类。before和after表示前后指针。我们在使用LinkedHashMap有序就是因此产生。

④分析LRU规则实现,最近访问放在双向链表最后面

  1. void afterNodeAccess(Node<K,V> e) { // 把当前节点e放到双向链表尾部
  2. LinkedHashMap.Entry<K,V> last;
  3. //accessOrder就是我们前面说的LRU控制,当它为true,同时e对象不是尾节点(如果访问尾节点就不需要设置,该方法就是把节点放置到尾节点)
  4. if (accessOrder && (last = tail) != e) {
  5. //用a和b分别记录该节点前面和后面的节点
  6. LinkedHashMap.Entry<K,V> p =
  7. (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
  8. //释放当前节点与后节点的关系
  9. p.after = null;
  10. //如果当前节点的前节点是空,
  11. if (b == null)
  12. //那么头节点就设置为a
  13. head = a;
  14. else
  15. //如果b不为null,那么b的后节点指向a
  16. b.after = a;
  17. //如果a节点不为空
  18. if (a != null)
  19. //a的后节点指向b
  20. a.before = b;
  21. else
  22. //如果a为空,那么b就是尾节点
  23. last = b;
  24. //如果尾节点为空
  25. if (last == null)
  26. //那么p为头节点
  27. head = p;
  28. else {
  29. //否则就把p放到双向链表最尾处
  30. p.before = last;
  31. last.after = p;
  32. }
  33. //设置尾节点为P
  34. tail = p;
  35. //LinkedHashMap对象操作次数+1
  36. ++modCount;
  37. }
  38. }

afterNodeAccess方法就是如何支持LRU规则的,如果在accessOrder为true的时候,节点调用这个函数,就会把该节点放置到最后面。put,get等都会调用这个函数来调整顺序,我手画了一张图来表示这个函数干了些什么: 

我们看看get方法中是否调用了此函数,以下是LinkedHashMap重写了HashMap的get方法:

  1. public V get(Object key) {
  2. Node<K,V> e;
  3. if ((e = getNode(hash(key), key)) == null)
  4. return null;
  5. if (accessOrder)//如果启用了LRU规则
  6. afterNodeAccess(e);//那么把该节点移到双向链表最后面
  7. return e.value;
  8. }

那么有些小伙伴就问了,那么put方法里面调用了嘛?肯定调用了,但是你不一定找得到,因为LinkedHashMap压根没有重写put方法,每次用LinkedHashMap的put方法的时候,其实你调用的都是HashMap的put方法。那为什么它也会执行afterNodeAccess()方法呢,因为这个方法HashMap就是存在的,但是没有实现,LinkedHashMap重写了afterNodeAccess()这个方法。下面给出HashMap的put局部方法:

  1. if (e != null) { // existing mapping for key
  2. V oldValue = e.value;
  3. if (!onlyIfAbsent || oldValue == null)
  4. e.value = value;
  5. afterNodeAccess(e);//把当前节点移动到双向链表最尾处
  6. return oldValue;
  7. }

其实这个方法在很多的调用都有,这里就不一一解释了。同时,LinkedHashMap对于红黑树的节点移动处理也有专门的方法,这里就不再深入讲解了,方式也是差不多。

⑤分析一个LinkedHashMap自带的移除最头(最老)数据的方法

  1. protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
  2. return false;
  3. }

LinkedHashMap有一个自带的移除最老数据的方法,但是这个方法永远是返回false,但是如果我们要实现,可以在继承的时候重写这个方法,给定一个条件就可以控制存储在LinkedHashMap中的最老数据何时删除。具体的在我以前博文多种方式实现缓存机制中有写过。触发这个删除机制,一般是在PUT一个数据进入的时候,但是LinkedHashMap并没有重写Put方法如何实现呢?在LinekdHashMap中,这个方法被包含在afterNodeInsertion()方法之中,而这个方法是重写了HashMap的,但是HashMap中并没有去实现它,所以在put的时候就会触发删除这个机制。

尾记

技术是不断前进的,或许在JDK1.8的时候我写的这些是有用的,但是下一个版本出来就不一定了。比如说前面几个版本中,LinkedHashMap是一个循环的双向链表,而且需要用init()方法进行初始化,但是后来都被移除了,以下是部分原本的linkedHashMap源码:

  1. void init() {
  2. header = new Entry<K,V>(-1, null, null, null);
  3. header.before = header.after = header; //循环的双向链表
  4. }

所以,在日常的学习中,尤其是技术,要与时俱进,在查询某个技术点的时候,千万要注意版本号,不一样的版本之间可能是天差地别的。

转载:https://blog.csdn.net/u012403290/article/details/70143443?locationNum=14&fps=1

linkedHashMap源码解析(JDK1.8)的更多相关文章

  1. 给jdk写注释系列之jdk1.6容器(5)-LinkedHashMap源码解析

    前面分析了HashMap的实现,我们知道其底层数据存储是一个hash表(数组+单向链表).接下来我们看一下另一个LinkedHashMap,它是HashMap的一个子类,他在HashMap的基础上维持 ...

  2. Java——LinkedHashMap源码解析

    以下针对JDK 1.8版本中的LinkedHashMap进行分析. 对于HashMap的源码解析,可阅读Java--HashMap源码解析 概述   哈希表和链表基于Map接口的实现,其具有可预测的迭 ...

  3. 源码解析JDK1.8-HashMap链表成环的问题解决方案

    前言 上篇文章详解介绍了HashMap在JDK1.7版本中链表成环的原因,今天介绍下JDK1.8针对HashMap线程安全问题的解决方案. jdk1.8 扩容源码解析 public class Has ...

  4. Java集合-ArrayList源码解析-JDK1.8

    ◆ ArrayList简介 ◆ ArrayList 是一个数组队列,相当于 动态数组.与Java中的数组相比,它的容量能动态增长.它继承于AbstractList,实现了List, RandomAcc ...

  5. ArrayList、CopyOnWriteArrayList源码解析(JDK1.8)

    本篇文章主要是学习后的知识记录,存在不足,或许不够深入,还请谅解. 目录 ArrayList源码解析 ArrayList中的变量 ArrayList构造函数 ArrayList中的add方法 Arra ...

  6. Map集合类(一.hashMap源码解析jdk1.8)

    java集合笔记一 java集合笔记二 java集合笔记三 jdk 8 之前,其内部是由数组+链表来实现的,而 jdk 8 对于链表长度超过 8 的链表将转储为红黑树 1.属性 //节点数组,第一次使 ...

  7. LinkedHashMap 源码解析

    概述: LinkedHashMap实现Map继承HashMap,基于Map的哈希表和链该列表实现,具有可预知的迭代顺序. LinedHashMap维护着一个运行于所有条目的双重链表结构,该链表定义了迭 ...

  8. Android——LruCache源码解析

    以下针对 Android API 26 版本的源码进行分析. 在了解LruCache之前,最好对LinkedHashMap有初步的了解,LruCache的实现主要借助LinkedHashMap.Lin ...

  9. 给jdk写注释系列之jdk1.6容器(8)-TreeSet&NavigableMap&NavigableSet源码解析

    TreeSet是一个有序的Set集合. 既然是有序,那么它是靠什么来维持顺序的呢,回忆一下TreeMap中是怎么比较两个key大小的,是通过一个比较器Comparator对不对,不过遗憾的是,今天仍然 ...

随机推荐

  1. 2018数学建模A题优秀论文:高温作业专用服装设计

    高温作业专用服装设计 摘 要 本文针对多层材料的高温作业服装的传热问题进行研究,综合考虑多种传热方式建立传热模型,并以此模型为基础解决了服装设计中各层材料最佳厚度的问题. 对于问题一,要求在热物性系数 ...

  2. Python第四次作业

    设计题1: 设计一个本月份日历,输出格式如下: 要求: 1.初始化start_day,end_day两个日期 from datetime import datetime start_day=datet ...

  3. centos虚拟机设置静态ip

    1.虚拟机网络配置设置为桥接模式 2.设置配置文件: vi /etc/sysconfig/network-scripts/ifcfg-ens33 DEVICE=ens33 HWADDR=:0c::c3 ...

  4. ios网络 -- HTTP请求 and 文件下载/断点下载

    一:请求 http://www.jianshu.com/p/8a90aa6bad6b 二:下载 iOS网络--『文件下载.断点下载』的实现(一):NSURLConnection http://www. ...

  5. Qt 学习之路 2(44):QFileSystemModel

    Home / Qt 学习之路 2 / Qt 学习之路 2(44):QFileSystemModel Qt 学习之路 2(44):QFileSystemModel  豆子  2013年2月21日  Qt ...

  6. JavaScript的高级知识---词法分析

    JavaScript的高级知识---词法分析 词法分析 词法分析方法: js运行前有一个类似编译的过程即词法分析,词法分析主要有三个步骤: 分析参数 再分析变量的声明 分析函数说明 函数在运行的瞬间, ...

  7. python学习之路---day06

    一:is 和 == 的区别 01)a b 两个变量 is 是比较变量的内存地址,如果地址相等,则返回True,如果不相等,则返回False == 是比较变量两边内容是否一样,如果一样则返回True,不 ...

  8. 最近做了一个短网址服务 di81.com

    最近做了一个短网址服务:   di81.com 项目中有一处需求,需要把长网址缩为短网址,把结果通过短信.微信等渠道推送给客户.刚开始直接使用网上现成的开放服务,然后在某个周末突然手痒想自己动手实现一 ...

  9. restFull接口实现web

    1. 模板引擎JSP的限制 在开始之前呢,我觉得我们有必要先去了解下 Spring Boot 2.0 官方文档中提到的如下内容: 模板引擎 除了REST Web服务之外,还可以使用Spring MVC ...

  10. 关于XML的验证(DTD与XSD)一点实践

    [转自] http://blog.chinaunix.net/uid-276853-id-366491.html 关于XML的验证一点实践 1)此方法是在XML文档中绑定对应的DTD文件来进行的 // ...