这篇文章我们开始分析LinkedHashMap的源码,LinkedHashMap继承了HashMap,也就是说LinkedHashMap是在HashMap的基础上扩展而来的,因此在看LinkedHashMap源码之前,读者有必要先去了解HashMap的源码,可以查看我上一篇文章的介绍《Java集合系列[3]----HashMap源码分析》。只要深入理解了HashMap的实现原理,回过头来再去看LinkedHashMap,HashSet和LinkedHashSet的源码那都是非常简单的。因此,读者们好好耐下性子来研究研究HashMap源码吧,这可是买一送三的好生意啊。在前面分析HashMap源码时,我采用以问题为导向对源码进行分析,这样使自己不会像无头苍蝇一样乱分析一通,读者也能够针对问题更加深入的理解。本篇我决定还是采用这样的方式对LinkedHashMap进行分析。

1. LinkedHashMap内部采用了什么样的结构?

可以看到,由于LinkedHashMap是继承自HashMap的,所以LinkedHashMap内部也还是一个哈希表,只不过LinkedHashMap重新写了一个Entry,在原来HashMap的Entry上添加了两个成员变量,分别是前继结点引用和后继结点引用。这样就将所有的结点链接在了一起,构成了一个双向链表,在获取元素的时候就直接遍历这个双向链表就行了。我们看看LinkedHashMap实现的Entry是什么样子的。

  1. private static class Entry<K,V> extends HashMap.Entry<K,V> {
  2. //当前结点在双向链表中的前继结点的引用
  3. Entry<K,V> before;
  4. //当前结点在双向链表中的后继结点的引用
  5. Entry<K,V> after;
  6.  
  7. Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
  8. super(hash, key, value, next);
  9. }
  10.  
  11. //从双向链表中移除该结点
  12. private void remove() {
  13. before.after = after;
  14. after.before = before;
  15. }
  16.  
  17. //将当前结点插入到双向链表中一个已存在的结点前面
  18. private void addBefore(Entry<K,V> existingEntry) {
  19. //当前结点的下一个结点的引用指向给定结点
  20. after = existingEntry;
  21. //当前结点的上一个结点的引用指向给定结点的上一个结点
  22. before = existingEntry.before;
  23. //给定结点的上一个结点的下一个结点的引用指向当前结点
  24. before.after = this;
  25. //给定结点的上一个结点的引用指向当前结点
  26. after.before = this;
  27. }
  28.  
  29. //按访问顺序排序时, 记录每次获取的操作
  30. void recordAccess(HashMap<K,V> m) {
  31. LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
  32. //如果是按访问顺序排序
  33. if (lm.accessOrder) {
  34. lm.modCount++;
  35. //先将自己从双向链表中移除
  36. remove();
  37. //将自己放到双向链表尾部
  38. addBefore(lm.header);
  39. }
  40. }
  41.  
  42. void recordRemoval(HashMap<K,V> m) {
  43. remove();
  44. }
  45. }

2. LinkedHashMap是怎样实现按插入顺序排序的?

  1. //父类put方法中会调用的该方法
  2. void addEntry(int hash, K key, V value, int bucketIndex) {
  3. //调用父类的addEntry方法
  4. super.addEntry(hash, key, value, bucketIndex);
  5. //下面操作是方便LRU缓存的实现, 如果缓存容量不足, 就移除最老的元素
  6. Entry<K,V> eldest = header.after;
  7. if (removeEldestEntry(eldest)) {
  8. removeEntryForKey(eldest.key);
  9. }
  10. }
  11.  
  12. //父类的addEntry方法中会调用该方法
  13. void createEntry(int hash, K key, V value, int bucketIndex) {
  14. //先获取HashMap的Entry
  15. HashMap.Entry<K,V> old = table[bucketIndex];
  16. //包装成LinkedHashMap自身的Entry
  17. Entry<K,V> e = new Entry<>(hash, key, value, old);
  18. table[bucketIndex] = e;
  19. //将当前结点插入到双向链表的尾部
  20. e.addBefore(header);
  21. size++;
  22. }

LinkedHashMap重写了它的父类HashMap的addEntry和createEntry方法。当要插入一个键值对的时候,首先会调用它的父类HashMap的put方法。在put方法中会去检查一下哈希表中是不是存在了对应的key,如果存在了就直接替换它的value就行了,如果不存在就调用addEntry方法去新建一个Entry。注意,这时候就调用到了LinkedHashMap自己的addEntry方法。我们看到上面的代码,这个addEntry方法除了回调父类的addEntry方法之外还会调用removeEldestEntry去移除最老的元素,这步操作主要是为了实现LRU算法,下面会讲到。我们看到LinkedHashMap还重写了createEntry方法,当要新建一个Entry的时候最终会调用这个方法,createEntry方法在每次将Entry放入到哈希表之后,就会调用addBefore方法将当前结点插入到双向链表的尾部。这样双向链表就记录了每次插入的结点的顺序,获取元素的时候只要遍历这个双向链表就行了,下图演示了每次调用addBefore的操作。由于是双向链表,所以将当前结点插入到头结点之前其实就是将当前结点插入到双向链表的尾部。

3. 怎样利用LinkedHashMap实现LRU缓存?

我们知道缓存的实现依赖于计算机的内存,而内存资源是相当有限的,不可能无限制的存放元素,所以我们需要在容量不够的时候适当的删除一些元素,那么到底删除哪个元素好呢?LRU算法的思想是,如果一个数据最近被访问过,那么将来被访问的几率也更高。所以我们可以删除那些不经常被访问的数据。接下来我们看看LinkedHashMap内部是怎样实现LRU机制的。

  1. public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V> {
  2. //双向链表头结点
  3. private transient Entry<K,V> header;
  4. //是否按访问顺序排序
  5. private final boolean accessOrder;
  6. ...
  7. public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
  8. super(initialCapacity, loadFactor);
  9. this.accessOrder = accessOrder;
  10. }
  11. //根据key获取value值
  12. public V get(Object key) {
  13. //调用父类方法获取key对应的Entry
  14. Entry<K,V> e = (Entry<K,V>)getEntry(key);
  15. if (e == null) {
  16. return null;
  17. }
  18. //如果是按访问顺序排序的话, 会将每次使用后的结点放到双向链表的尾部
  19. e.recordAccess(this);
  20. return e.value;
  21. }
  22. private static class Entry<K,V> extends HashMap.Entry<K,V> {
  23. ...
  24. //将当前结点插入到双向链表中一个已存在的结点前面
  25. private void addBefore(Entry<K,V> existingEntry) {
  26. //当前结点的下一个结点的引用指向给定结点
  27. after = existingEntry;
  28. //当前结点的上一个结点的引用指向给定结点的上一个结点
  29. before = existingEntry.before;
  30. //给定结点的上一个结点的下一个结点的引用指向当前结点
  31. before.after = this;
  32. //给定结点的上一个结点的引用指向当前结点
  33. after.before = this;
  34. }
  35. //按访问顺序排序时, 记录每次获取的操作
  36. void recordAccess(HashMap<K,V> m) {
  37. LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;
  38. //如果是按访问顺序排序
  39. if (lm.accessOrder) {
  40. lm.modCount++;
  41. //先将自己从双向链表中移除
  42. remove();
  43. //将自己放到双向链表尾部
  44. addBefore(lm.header);
  45. }
  46. }
  47. ...
  48. }
  49. //父类put方法中会调用的该方法
  50. void addEntry(int hash, K key, V value, int bucketIndex) {
  51. //调用父类的addEntry方法
  52. super.addEntry(hash, key, value, bucketIndex);
  53. //下面操作是方便LRU缓存的实现, 如果缓存容量不足, 就移除最老的元素
  54. Entry<K,V> eldest = header.after;
  55. if (removeEldestEntry(eldest)) {
  56. removeEntryForKey(eldest.key);
  57. }
  58. }
  59. //是否删除最老的元素, 该方法设计成要被子类覆盖
  60. protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
  61. return false;
  62. }
  63. }

为了更加直观,上面贴出的代码中我将一些无关的代码省略了,我们可以看到LinkedHashMap有一个成员变量accessOrder,该成员变量记录了是否需要按访问顺序排序,它提供了一个构造器可以自己指定accessOrder的值。每次调用get方法获取元素式都会调用e.recordAccess(this),该方法会将当前结点移到双向链表的尾部。现在我们知道了如果accessOrder为true那么每次get元素后都会把这个元素挪到双向链表的尾部。这一步的目的是区别出最常使用的元素和不常使用的元素,经常使用的元素放到尾部,不常使用的元素放到头部。我们再回到上面的代码中看到每次调用addEntry方法时都会判断是否需要删除最老的元素。判断的逻辑是removeEldestEntry实现的,该方法被设计成由子类进行覆盖并重写里面的逻辑。注意,由于最近被访问的结点都被挪动到双向链表的尾部,所以这里是从双向链表头部取出最老的结点进行删除。下面例子实现了一个简单的LRU缓存。

  1. public class LRUMap<K, V> extends LinkedHashMap<K, V> {
  2.  
  3. private int capacity;
  4.  
  5. LRUMap(int capacity) {
  6. //调用父类构造器, 设置为按访问顺序排序
  7. super(capacity, 1f, true);
  8. this.capacity = capacity;
  9. }
  10.  
  11. @Override
  12. public boolean removeEldestEntry(Map.Entry<K, V> eldest) {
  13. //当键值对大于等于哈希表容量时
  14. return this.size() >= capacity;
  15. }
  16.  
  17. public static void main(String[] args) {
  18. LRUMap<Integer, String> map = new LRUMap<Integer, String>(4);
  19. map.put(1, "a");
  20. map.put(2, "b");
  21. map.put(3, "c");
  22. System.out.println("原始集合:" + map);
  23. String s = map.get(2);
  24. System.out.println("获取元素:" + map);
  25. map.put(4, "d");
  26. System.out.println("插入之后:" + map);
  27. }
  28.  
  29. }

结果如下:

注:以上全部分析基于JDK1.7,不同版本间会有差异,读者需要注意

Java集合系列[4]----LinkedHashMap源码分析的更多相关文章

  1. java集合系列之LinkedList源码分析

    java集合系列之LinkedList源码分析 LinkedList数据结构简介 LinkedList底层是通过双端双向链表实现的,其基本数据结构如下,每一个节点类为Node对象,每个Node节点包含 ...

  2. java集合系列之ArrayList源码分析

    java集合系列之ArrayList源码分析(基于jdk1.8) ArrayList简介 ArrayList时List接口的一个非常重要的实现子类,它的底层是通过动态数组实现的,因此它具备查询速度快, ...

  3. Java集合系列:-----------03ArrayList源码分析

    上一章,我们学习了Collection的架构.这一章开始,我们对Collection的具体实现类进行讲解:首先,讲解List,而List中ArrayList又最为常用.因此,本章我们讲解ArrayLi ...

  4. Java集合系列[1]----ArrayList源码分析

    本篇分析ArrayList的源码,在分析之前先跟大家谈一谈数组.数组可能是我们最早接触到的数据结构之一,它是在内存中划分出一块连续的地址空间用来进行元素的存储,由于它直接操作内存,所以数组的性能要比集 ...

  5. Java集合系列[3]----HashMap源码分析

    前面我们已经分析了ArrayList和LinkedList这两个集合,我们知道ArrayList是基于数组实现的,LinkedList是基于链表实现的.它们各自有自己的优劣势,例如ArrayList在 ...

  6. Java集合系列[2]----LinkedList源码分析

    上篇我们分析了ArrayList的底层实现,知道了ArrayList底层是基于数组实现的,因此具有查找修改快而插入删除慢的特点.本篇介绍的LinkedList是List接口的另一种实现,它的底层是基于 ...

  7. java多线程系列(九)---ArrayBlockingQueue源码分析

    java多线程系列(九)---ArrayBlockingQueue源码分析 目录 认识cpu.核心与线程 java多线程系列(一)之java多线程技能 java多线程系列(二)之对象变量的并发访问 j ...

  8. Java并发系列[2]----AbstractQueuedSynchronizer源码分析之独占模式

    在上一篇<Java并发系列[1]----AbstractQueuedSynchronizer源码分析之概要分析>中我们介绍了AbstractQueuedSynchronizer基本的一些概 ...

  9. Java并发系列[3]----AbstractQueuedSynchronizer源码分析之共享模式

    通过上一篇的分析,我们知道了独占模式获取锁有三种方式,分别是不响应线程中断获取,响应线程中断获取,设置超时时间获取.在共享模式下获取锁的方式也是这三种,而且基本上都是大同小异,我们搞清楚了一种就能很快 ...

随机推荐

  1. android中的五大布局(控件的容器,可以放button等控件)

    一.android中五大布局相当于是容器,这些容器里可以放控件也可以放另一个容器,子控件和布局都需要制定属性. 1.相对布局:RelativeLayout @1控件默认堆叠排列,需要制定控件的相对位置 ...

  2. P神的SDFZ考试题 C题

                                                                      探险[问题描述]          探险家小 T 好高兴! X 国要 ...

  3. Gulp-静态网页模块化

    前言: 在做纯静态页面开发的过程中,难免会遇到一些的尴尬问题.比如:整套代码有50个页面,其中有40个页面顶部和底部模块相同.那么同样的两段代码我们复制了40遍(最难受的方法).然后,这个问题就这样解 ...

  4. C# DataGridView 的UserDeletingRow事件,删除

    DialogResult dr = MessageBox.Show("确认删除记录吗?", "提示", MessageBoxButtons.YesNo);    ...

  5. MySQL一对一:一对多:多对多: 实例!!!!

    学生表和课程表可以多对多 一个学生可以学多门课程 一门课程可以有多个学生: 多对多 *** 一个学生对应一个班级 一个班级对应多个学生: 一对多 *** 一个老师对应多个学生 多个学生对应一个老师:一 ...

  6. 面向对象 初级版 (Preview) 未完

    概述: 面向过程:根据业务逻辑从上到下写垒代码 函数式:将某功能代码封装到函数里,日后使用无需重复编写,直接调用韩顺即可. 面向对象: 对函数进行分类和封装,让开发'更快更强' 面向对象和面向过程的通 ...

  7. Linux程序包管理rpm与yum

    Linux程序包管理 Linux中软件的安装主要有两种形式:一种是直接下载源代码包自行编译后安装,另一种直接获取rpm软件包进行安装. 程序的组成部分: 二进制程序:程序的主体文件,比如我们运行一个l ...

  8. Python新式类与经典类的区别

    1.新式类与经典类 在Python 2及以前的版本中,由任意内置类型派生出的类(只要一个内置类型位于类树的某个位置),都属于“新式类”,都会获得所有“新式类”的特性:反之,即不由任意内置类型派生出的类 ...

  9. 基于MongoDb官方C#驱动封装MongoDbCsharpHelper类(CRUD类)

    近期工作中有使用到 MongoDb作为日志持久化对象,需要实现对MongoDb的增.删.改.查,但由于MongoDb的版本比较新,是2.4以上版本的,网上已有的一些MongoDb Helper类都是基 ...

  10. CentOS修改系统时间

    CentOS修改系统时间 操作: 1. date –s '1987-05-02 10:10:10' 2. clock –w //将日期写入CMOS 补充: 修改Linux时间一般涉及到3个命令: 1. ...