LinkedHashMap深度解析

时间:2024-11-17 07:54:38

概述

在平时开发的过程中,大部分都是使用HashMap存储key value结构的数据,但是它是没有顺序的,如果你想要一个有顺序的Map,这时候LinkedHashMap就闪亮登场, 本篇文章主要基于jdk1.8学习下LinkedHashMap的功能和原理。在学习LinkedHashMap你需要对HashMap底层实现和源码有一定了解。

介绍

LinkedHashMap是一个有顺序的Hash表,它可以是元素插入顺序或者访问顺序。

  • LinkedHashMap最大的特点是有顺序的
  • LinkedHashMap的key 和value都可以为空
  • LinkedHashMap不是线程安全


 

以上是LinkedHashMap的类结构图:

  • 继承了HashMap,在HashMap的功能基础上,增加了访问顺序的能力

使用案例

LinkedHashMap基于插入顺序

  1. @Test
  2. public void test1() {
  3. // 创建默认的 LinkedHashMap
  4. Map<String, String> map = new LinkedHashMap<>();
  5. // 插入
  6. ("1", "1");
  7. ("2", "2");
  8. ("5", "5");
  9. ("4", "4");
  10. (map);
  11. // 访问
  12. ("2");
  13. ("1");
  14. (map);
  15. }

 运行结果:

 

小结:

  1. 默认LinkedHashMap是维护插入顺序,访问不会改变顺序

LinkedHashMap基于插入顺序和访问顺序

  1. @Test
  2. public void test2() {
  3. // 创建 LinkedHashMap, accessOrder设置为true
  4. Map<String, String> map = new LinkedHashMap<String, String>(16, 0.75f, true);
  5. // 插入
  6. ("1", "1");
  7. ("2", "2");
  8. ("5", "5");
  9. ("4", "4");
  10. (map);
  11. // 访问
  12. ("2");
  13. ("1");
  14. (map);
  15. }

 运行结果:

 

小结:

  1. 通过3个参数的构造函数可以创建出基于插入顺序+访问顺序的LinkedHashMap

核心机制

实现机制

LinkedHashMap继承了HashMap, 那么它也就拥有了HashMap的一些机制,包括扩容机制、转换成红黑树等都是一样的逻辑,但是LinkedHashMap拥有了一个更强的能力,就是有顺序,那么它是怎么维护节点的顺序呢?


 

LinkedHashMap额外维护了一个双向链表,在在每次插入数据,或者访问、修改数据时,会对这个双向链表增加节点、或调整链表的节点顺序,从而保证这个有序性。

源码解析

成员变量

通过成员变量我们看出LinkedHashMap底层的数据结构。

  1. // 双向链表的头部节点(最早插入的,年纪最大的节点)
  2. transient <K,V> head;
  3. // 双向链表的尾部节点(最新插入的,年纪最小的节点)
  4. transient <K,V> tail;
  5. // 用于控制访问顺序,为true时,按插入顺序;为false时,按访问顺序
  6. final boolean accessOrder;

 

LinkedHashMap继承自HashMap,所以内部存储数据的方式和HashMap一样,使用数组加链表(红黑树)的结构存储数据,LinkedHashMap和HashMap相比,额外的维护了一个双向链表,用于存储节点的顺序。这个双向链表的类型为:


 

  1. static class Entry<K,V> extends HashMap.Node<K,V> {
  2. Entry<K,V> before, after;
  3. Entry(int hash, K key, V value, Node<K,V> next) {
  4. super(hash, key, value, next);
  5. }
  6. }

继承自HashMap的Node类,新增了before和after属性,用于维护前继和后继节点,以此形成双向链表。

构造方法

LinkedHashMap构造和HashMap多个一个accessOrder, 用于控制访问顺序。

  1. public LinkedHashMap(int initialCapacity,
  2. float loadFactor,
  3. boolean accessOrder) {
  4. super(initialCapacity, loadFactor);
  5. this.accessOrder = accessOrder;
  6. }

只有这个构造函数可以通过参数修改accessOrder。

当accessOrder属性为true时,元素按访问顺序排列,即最近访问的元素会被移动到双向列表的末尾。所谓的“访问”并不是只有get方法,符合“访问”一词的操作有put、putIfAbsent、get、getOrDefault、compute、computeIfAbsent、computeIfPresent和merge方法。

关键方法

put方法

LinkedHashMap并没有put方法,而是使用的父类HashMap的put方法,那么它是怎么做到维护链表的呢?答案就是重写,HashMap提供了一些可以重写的入口。关于HashMap的源码这里就不详细分析了。

put方法最后是调到HashMap#putVal。

 newNode方法用于创建链表节点,LinkedHashMap重写了newNode方法:

  1. Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
  2. // 创建实例
  3. <K,V> p =
  4. new LinkedHashMap.Entry<K,V>(hash, key, value, e);
  5. // 将新节点放入LinkedHashMap维护的双向链表尾部
  6. linkNodeLast(p);
  7. return p;
  8. }
  9. private void linkNodeLast(<K,V> p) {
  10. <K,V> last = tail;
  11. tail = p;
  12. // 如果尾节点为空,说明双向链表是空的,所以将该节点赋值给头节点,双向链表得以初始化
  13. if (last == null)
  14. head = p;
  15. else {
  16. // 否则将该节点放到双向链表的尾部
  17. = last;
  18. = p;
  19. }
  20. }

 

所以可以看到,在put方法的时候调用newNode方法,LinkedHashMap会维护这个双向链表。

LinkedHashMap也重写了afterNodeAccess方法,顾名思义,就是当节点被访问后执行某些操作。

  1. void afterNodeAccess(Node<K,V> e) { // move node to last
  2. <K,V> last;
  3. // 如果accessOrder属性为true,并且当前节点不是双向链表的尾节点的话执行if内逻辑
  4. if (accessOrder && (last = tail) != e) {
  5. // 下面的逻辑就是将当前节点移动到双向链表的尾部,并且改变相关节点的前继后继关系
  6. <K,V> p =
  7. (<K,V>)e, b = , a = ;
  8. = null;
  9. if (b == null)
  10. head = a;
  11. else
  12. = a;
  13. if (a != null)
  14. = b;
  15. else
  16. last = b;
  17. if (last == null)
  18. head = p;
  19. else {
  20. = last;
  21. = p;
  22. }
  23. tail = p;
  24. ++modCount;
  25. }
  26. }

 LinkedHashMap也重写了afterNodeInsertion方法,执行节点插入成功后的逻辑:

  1. void afterNodeInsertion(boolean evict) { // possibly remove eldest
  2. <K,V> first;
  3. // 如果头部节点不为空并且removeEldestEntry方法返回true的话
  4. if (evict && (first = head) != null && removeEldestEntry(first)) {
  5. // 获取头部节点的key
  6. K key = ;
  7. // 调用父类HashMap的removeNode方法,删除这个key的节点,也就是第一个节点
  8. removeNode(hash(key), key, null, false, true);
  9. }
  10. }

 

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. // 当accessOrder属性为true时,将key对应的键值对节点移动到双向列表的尾部
  6. if (accessOrder)
  7. afterNodeAccess(e);
  8. return ;
  9. }

这里的afterNodeAccess方法上面讲过了,用来节点访问时候的回调,需要通过构造方法设置accessOrder的属性为true才会生效。

remove方法

LinkedHashMap没有重写remove方法,查看HashMap的remove相关方法发现如下:


LinkedHashMap重写了这个afterNodeRemoval方法: 

  1. // 逻辑简单,改变节点的前继后继引用
  2. void afterNodeRemoval(Node<K,V> e) { // unlink
  3. <K,V> p =
  4. (<K,V>)e, b = , a = ;
  5. = = null;
  6. if (b == null)
  7. head = a;
  8. else
  9. = a;
  10. if (a == null)
  11. tail = b;
  12. else
  13. = b;
  14. }

 

通过该方法,我们就从LinkedHashMap的双向链表中删除对应的节点。

总结

本文主要分析了LinkedHashMap的有序性的这一特性,从源码层面理解它是如何保证有序的,但是前提希望大家能够对HashMap的实现或者源码有一定的基础,你才能够更好的理解LinkedHashMap。