LinkedHashMap深度解析

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: LinkedHashMap深度解析

概述


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


介绍


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

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

1671186076468.jpg

以上是LinkedHashMap的类结构图:

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


使用案例


LinkedHashMap基于插入顺序

@Test
    public void test1() {
        // 创建默认的 LinkedHashMap
        Map<String, String> map = new LinkedHashMap<>();
        // 插入
        map.put("1", "1");
        map.put("2", "2");
        map.put("5", "5");
        map.put("4", "4");
        System.out.println(map);
        // 访问
        map.get("2");
        map.get("1");
        System.out.println(map);
    }

运行结果:

1671186088437.jpg

小结:

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

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

@Test
    public void test2() {
        // 创建 LinkedHashMap, accessOrder设置为true
        Map<String, String> map = new LinkedHashMap<String, String>(16, 0.75f, true);
        // 插入
        map.put("1", "1");
        map.put("2", "2");
        map.put("5", "5");
        map.put("4", "4");
        System.out.println(map);
        // 访问
        map.get("2");
        map.get("1");
        System.out.println(map);
    }

运行结果:

1671186102509.jpg

小结:

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


核心机制


实现机制


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

1671186125586.jpg

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


源码解析


成员变量


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

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

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

1671186140340.jpg

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


构造方法


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

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

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

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


关键方法


put方法


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

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

1671186174767.jpg

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

Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
        // 创建LinkedHashMap.Entry实例
        LinkedHashMap.Entry<K,V> p =
            new LinkedHashMap.Entry<K,V>(hash, key, value, e);
         // 将新节点放入LinkedHashMap维护的双向链表尾部
        linkNodeLast(p);
        return p;
    }
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    LinkedHashMap.Entry<K,V> last = tail;
    tail = p;
    // 如果尾节点为空,说明双向链表是空的,所以将该节点赋值给头节点,双向链表得以初始化
    if (last == null)
        head = p;
    else {
        // 否则将该节点放到双向链表的尾部
        p.before = last;
        last.after = p;
    }
}

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

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

void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
        // 如果accessOrder属性为true,并且当前节点不是双向链表的尾节点的话执行if内逻辑
        if (accessOrder && (last = tail) != e) {
              // 下面的逻辑就是将当前节点移动到双向链表的尾部,并且改变相关节点的前继后继关系
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            p.after = null;
            if (b == null)
                head = a;
            else
                b.after = a;
            if (a != null)
                a.before = b;
            else
                last = b;
            if (last == null)
                head = p;
            else {
                p.before = last;
                last.after = p;
            }
            tail = p;
            ++modCount;
        }
    }

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

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


get方法


LinkedHashMap重写了HashMap的get方法,逻辑如下:

public V get(Object key) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
        // 当accessOrder属性为true时,将key对应的键值对节点移动到双向列表的尾部
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
    }

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


remove方法


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

1671186198706.jpg

LinkedHashMap重写了这个afterNodeRemoval方法:

// 逻辑简单,改变节点的前继后继引用
void afterNodeRemoval(Node<K,V> e) { // unlink
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        p.before = p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;
        if (a == null)
            tail = b;
        else
            a.before = b;
    }

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


总结


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

目录
相关文章
|
7月前
|
存储 缓存 Java
Java LinkedHashMap:保持插入顺序的哈希表解析
Java LinkedHashMap:保持插入顺序的哈希表解析
148 0
|
存储 Java
LinkedHashMap源码解析
上文提到多次LRUCache,其中LRU是Least Recently Used的缩写,其实就是想在有限的存储空间里保留更多有价值数据的一种方式。其实现的依旧就是最佳被使用的数据将来还被使用的概率更高,这个现象在计算机领域非常明显,很多优化就是基于此的。LRUCache就是这样一种存储的实现,它的难点就在于如何高效地剔除掉最旧的数据,以及如何维护数据的新旧度。
55 0
|
安全
LinkedHashMap源码解析(下)
LinkedHashMap维护插入的顺序。
174 0
LinkedHashMap源码解析(下)
|
存储
LinkedHashMap源码解析(上)
LinkedHashMap维护插入的顺序。
175 0
LinkedHashMap源码解析(上)
|
算法 Java
Java基础之LinkedHashMap源码解析
从源码解析LinkedHashMap的原理
1657 0
|
存储
LinkedHashMap 源码解析
概述: LinkedHashMap实现Map继承HashMap,基于Map的哈希表和链该列表实现,具有可预知的迭代顺序。 LinedHashMap维护着一个运行于所有条目的双重链表结构,该链表定义了迭代顺序,可以是插入或者访问顺序。
975 0
|
Java 编译器 安全
java LinkedHashMap源码解析
本源码解析是基于JDK1.7,本篇与HashMap源码解析较强的关联性 LinkedHashMap概要 LinkedHashMap是基于HashTable与LinkedList原理实现的 HashMap是基于数组的...
1132 0
|
16天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
45 2

推荐镜像

更多