LinkedHashMap源码学习

简介: LinkedHashMap源码学习

一、介绍

首先声明一点,在学习LinkedHashMap源码时,HashMap的源码是必需的前置知识点,对HashMap的源码不了解的同学请移步HashMap源码。也希望能对模版方法的设计模式有一定的了解


在往期文章中,我们从源码分析了java集合框架中Map一族的HashMapTreeMap,他们都有各自的特点,如HashMap是无序的,TreeMap是利用底层红黑树的特点将键值对中的key进行排序按照排序结果进行遍历的。今天我们介绍Map一族的另一个实现LinkedHashMap

LinkedHashMap是的底层是双向链表+哈希表,从命名来看,它是双向链表与HashMap的结合体,就是在HashMap的基础上,将每个节点按照一定的顺序(插入顺序访问顺序)通过双向链表将这些节点串起来。

访问顺序是指调用put()putIfAbsent()get()getOrDefault()compute()computeIfAbsent()computeIfPresent()merge()方法会导致对相应键值对的访问(假设调用完成后该条目存在)

基于以上介绍,我们可以想象出来HashMap和LinkedHashMap的对比图如下所示

HashMap和LinkedHashMap对比图.png

从上图同学们是否觉得LinkedHashMap的结构要比HashMap复杂很多 ?通常我们从两个角度来看LinkedHashMap:

  • HashMap的角度

    从该角度看LinkedHashMap得到的结果如下图

    从HashMap的角度看.png

  • 双向链表的角度

    从该角度看LinkedHashMap得到的结果如下图

    从双向链表的角度看.png

二、使用场景

在jdk中,LinkedHashMap是实现LRU(最近最少使用)算法的推荐底层实现。

文档:A special constructor is provided to create a linked hash map whose order of iteration is the order in which its entries were last accessed, from least-recently accessed to most-recently (access-order). This kind of map is well-suited to building LRU caches.

翻译:提供了一个特殊的构造函数来创建一个链接哈希映射,其迭代顺序是其条目最后一次访问的顺序,从最近最少访问到最近访问(访问顺序)。这种map非常适合建造LRU的缓存

三、类的声明

我们来看一下LinkedHashMap的声明,可以大致了解他的功能。

public class LinkedHashMap<K,V> extends HashMap<K,V>
                                implements Map<K,V>

从类的声明来看

  • 继承了HashMap,因此LinkedHashMap是HashMap的扩展。这正是学习LinkedHashMap源码前必须学习HashMap源码的原因。
  • 实现了Map接口,说明LinkedHashMap是一个以<key,value>键值对存储数据的结构
  • 实现了Cloneable接口(由父类HashMap实现),提供了对象克隆方法,但请注意,是浅克隆
  • 实现了Serializable接口(由父类HashMap实现),支持序列化

下面是LinkedHashMap类的继承关系图

继承关系图.png

四、内部类Entry

在LinkedHashMap中,将键值对封装成节点的类为其内部类Entry,该内部类继承于HashMap的内部类Node,如下图所示

内部类的继承关系图.png

基于HashMap底层为哈希表,以及LinkedHashMap是HashMap的扩展这两个事实,从上图我们可以得知,LinkedHashMap的内部类EntryHashMap的内部类Node的扩展,所以

  • Node内部类和TreeNode内部类分别作为HashMap的普通节点和红黑树节点。
  • Entry内部类和TreeNode内部类分别作为LinkedHashMap的普通节点和红黑树节点。

下面我们看一下,LinkedHashMap的内部类EntryHashMap的内部类Node做了哪些扩展

// HashMap的内部类Node
static class Node<K,V> implements Map.Entry<K,V> {
   
   
    final int hash;
    final K key;
    V value;
    Node<K,V> next;

    Node(int hash, K key, V value, Node<K,V> next) {
   
   
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }
    // ...
}

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

从上面源码中可以看到,在Node的基础上,Entry添加了两个属性:beforeafter,分别表示当前节点的前驱节点和后继节点,也正因此,LinkedHashMap将HashMap的哈希表结构中的每一个节点关联起来形成一个双向链表

五、成员变量

LinkedHashMap只负责维护双向链表,因此只提供了和双向链表相关的属性:头节点尾节点,而对哈希表的维护由其父类 HashMap维护。

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

// true-表示该双向链表是按照访问顺序形成的
// false-表示该双向链表是按照插入顺序形成的
// 该字段对双向链表的遍历影响极大。
final boolean accessOrder;

六、构造方法

  • 无参构造

    可以看到,先调用父类HashMap的无参构造,然后定义默认的遍历顺序为插入顺序

    public LinkedHashMap() {
         
         
        super();
        accessOrder = false;
    }
    
  • 通过初始容量实例化

    调用父类的构造方法HashMap(int initialCapacity),然后定义默认的遍历顺序为插入顺序

    public LinkedHashMap(int initialCapacity) {
         
         
        super(initialCapacity);
        accessOrder = false;
    }
    
  • 通过初始容量加载因子实例化

    调用父类的构造方法HashMap(int initialCapacity, float loadFactor),然后定义默认的遍历顺序为插入顺序

    public LinkedHashMap(int initialCapacity, float loadFactor) {
         
         
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }
    
  • 通过初始容量加载因子访问顺序实例化

    调用父类的构造方法HashMap(int initialCapacity, float loadFactor),然后定义遍历顺序,false为插入顺序,true为访问顺序

    该构造方法就是为了实现LRU(最近最少使用)算法而存在的。

    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
         
         
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }
    
  • 通过传入一个Map集合实例化

    先调用父类HashMap的无参构造,定义默认的遍历顺序为插入顺序,再调用父类的putMapEntries()方法将map集合批量插入哈希表中。

    等同于调用父类的HashMap(Map<? extends K, ? extends V> m)构造方法,再确定默认的遍历顺序为插入顺序

    public LinkedHashMap(Map<? extends K, ? extends V> m) {
         
         
        super();
        accessOrder = false;
        putMapEntries(m, false);
    }
    

七、linkNodeLast()

该方法从命名上也不难看出,该方法就是将指定节点添加到双向链表的尾部

读过LinkedList源码或对双向链表有所了解的同学想必没什么问题,就是把原尾节点作为指定节点的前驱,把指定节点作为原尾节点的后继,还要把指定节点设置为尾节点

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

八、newNode()

在讲解HashMap的putVal()方法保存键值对时,通过调用newNode()方法将键值对构造成一个节点对象保存在哈希表中。虽然LinkedHashMap也是通过HashMap的putVal()方法保存键值对,但是需要对newNode()方法进行重写,以构造一个Entry类对象作为哈希表的节点。这是设计模式中模版方法的体现。

在本篇文章中,我们只介绍LinkedHashMap在构造Node节点时做了哪些事,至于putVal()方法的其他过程,我们依然参考HashMapputVal()方法。

在下面的源码中我们看到,在此方法中,构造出来的Node节点对象的实际类型为LinkedHashMap.Entry,然后维护双向链表通过调用linkNodeLast()放将新节点放在双向链表的尾部。

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);
    linkNodeLast(p);
    return p;
}

以下为HashMap中的newNode()方法,方便我们比较

Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
   
   
    return new Node<>(hash, key, value, next);
}

九、newTreeNode()

该方法与上面的newNode()方法类似,唯一区别就是构造出来的Node节点的实际类型为TreeNode类型,然后依然是维护双向链表通过调用linkNodeLast()放将新节点放在双向链表的尾部。

TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
   
   
    TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
    linkNodeLast(p);
    return p;
}

以下为HashMap中的newTreeNode()方法,方便我们比较

TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
   
   
    return new TreeNode<>(hash, key, value, next);
}

十、afterNodeAccess()

该方法在访问节点后调用,该方法作为回调函数在父类HashMap中当节点被访问时调用

调用put()putIfAbsent()get()getOrDefault()compute()computeIfAbsent()computeIfPresent()merge()方法会导致对相应键值对的访问(假设调用完成后该条目存在)

通过源码的分析我们知道,在构造LinkedHashMap实例时我们指定遍历顺序为访问顺序,则对一个节点发生访问后,该方法会将该节点设置为双向链表的尾节点。这是实现LRU算法的关键一环。

// 如果遍历顺序为访问顺序,则将指定节点设置为双向链表的尾节点,否则直接返回
void afterNodeAccess(Node<K,V> e) {
   
   
    LinkedHashMap.Entry<K,V> last;
    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;
    }
}

十一、afterNodeInsertion()

该方法在插入节点后调用,该方法作为回调函数在父类HashMap中当节点被插入时调用

调用put()putIfAbsent()compute()computeIfAbsent()merge()方法会导致对相应键值对的插入

在LinkedHashMap中,我们需要知道的是在节点插入到哈希表中以后,需要通过afterNodeInsertion()方法做什么事情?

从源码的分析中可以看出,该方法就是为了将双向链表中第一个节点通过removeNode()方法从哈希表中删除,但是要满足三个条件:

  • evict参数值为true,这取决于调用方传入的参数
  • 双向链表不为空
  • removeEldestEntry()方法返回值为true,该方法用来判断是否要移除指定的元素。

所以,该方法的目的就是在插入一个元素后,是否要删除存在时间最长的元素。该方法在LRU(最近最少使用)删除算法中有着关键的作用。

由于在LinkedHashMap中removeEldestEntry()方法的返回值为false,因此该方法实际不发生任何操作。

void afterNodeInsertion(boolean evict) {
   
    // possibly remove eldest
    LinkedHashMap.Entry<K,V> first;
    if (evict && (first = head) != null && removeEldestEntry(first)) {
   
   
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}


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

十二、afterNodeRemoval()

该方法在删除节点后调用,该方法作为回调函数在父类HashMap中当节点被删除时调用

在LinkedHashMap中,我们需要知道的是在节点从哈希表中删除以后,需要通过afterNodeRemoval()方法做什么事情?

通过对源码的分析,我们看到,该方法做的就是把从哈希表中删除的节点e,从双向链表中删除

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的大多数常用方法都继承自父类HashMap,在这里不做介绍了,请移步HashMap源码,但由于LinkedHashMap的特性,对继承自父类的某些方法进行了重写

  • clear()

    清空节点

    先调用父类的clear()方法将哈希表清空,再通过head = tail = null清空双向链表

    public void clear() {
         
         
        super.clear();
        // 清空双向链表
        head = tail = null;
    }
    
  • containsValue()

    判断是否存在包含指定value值的键值对节点

    从下面源码可以看出,由于LinkedHashMap将哈希表中的各个节点又额外连接成双向链表,因此可以直接通过双向链表来遍历所有节点,而不是像HashMap的containsValue()方法一样去遍历哈希表。

    public boolean containsValue(Object value) {
         
         
        // 遍历双向链表
        for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after) {
         
         
            V v = e.value;
            if (v == value || (value != null && value.equals(v)))
                return true;
        }
        return false;
    }
    
    // HashMap的containsValue()方法
    public boolean containsValue(Object value) {
         
         
        Node<K,V>[] tab; V v;
        if ((tab = table) != null && size > 0) {
         
         
            // 遍历哈希表
            for (int i = 0; i < tab.length; ++i) {
         
         
                for (Node<K,V> e = tab[i]; e != null; e = e.next) {
         
         
                    if ((v = e.value) == value ||
                        (value != null && value.equals(v)))
                        return true;
                }
            }
        }
        return false;
    }
    
  • get()

    根据指定的key值获取对应的键值对的value值。

    从下面源码看出,LinkedHashMap的get()方法比其父类HashMap的get()方法多了一个步骤:即如果当前LinkedHashMap的遍历顺序为按访问顺序遍历,则会调用afterNodeAccess()方法对获取的键值对进行回调处理。

    public V get(Object key) {
         
         
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
        if (accessOrder)
            // 将访问的节点移动到双向链表尾部
            afterNodeAccess(e);
        return e.value;
    }
    
    // HashMap的get()方法
    public V get(Object key) {
         
         
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
    

十四、总结

  • 底层实现为哈希表+双向链表,是对HashMap的扩展。对哈希表的操作由其父类HashMap实现,对双向链表的操作由LinkedHashMap实现。
  • LinkedHashMap的有序分两种,一是根据每个节点的访问顺序实现的,二是根据每个节点的插入顺序(默认)实现的
  • 线程不安全




纸上得来终觉浅,绝知此事要躬行。

————————————————我是万万岁,我们下期再见————————————————

相关文章
|
Java
0-1背包问题(Java详解)(动态规划)至少与恰好
0-1背包问题(Java详解)(动态规划)至少与恰好
203 1
|
JSON Linux 网络安全
一文搞定:whois数据库查询域名信息(WHOIS)
一文搞定:whois数据库查询域名信息(WHOIS)
3875 0
一文搞定:whois数据库查询域名信息(WHOIS)
|
算法 搜索推荐 计算机视觉
图片相似度计算及检索调研
图片相似度计算和相似图片搜索,是图片识别领域两个常见的应用场景。例如搜索相似商品,和相似的图片,在百度、淘宝中都有应用。在某些业务中,也存在对图片相似度的计算和判断。因此,在这里简单介绍一下相关算法。
1898 0
|
5月前
|
JSON 供应链 API
商品条码查询 API 实战指南:掌握商品“唯一身份标识”
商品条码查询API简介:基于1974年诞生的条码技术,该API通过输入13/14位条码,快速获取商品基本信息(名称、品牌、规格等)和成分信息(营养成分、配料表等)。其核心功能包括商品条码查询接口与成分查询接口,广泛应用于零售、电商、物流及健康饮食等领域。支持HTTP POST请求,提供便捷的代码调用示例。作为数字化转型的重要工具,它不仅方便消费者查询商品详情,还助力商家优化库存管理与销售流程,提升运营效率。
1282 3
|
监控 Java 微服务
spring 熔断机制
spring 熔断机制
310 0
|
9月前
|
Rust 前端开发 算法
java中如何实现单链表反转
本文介绍了单向链表的创建及其反转的三种实现方法。首先,通过`DataNode`类构建了一个包含10个节点的单向链表,并提供了链表的打印功能。接着,分别使用递归、遍历和借助栈的方式实现了链表反转。递归方法简单但受限于栈深度(最大约12000个节点),遍历方法通用且效率最高,而借助栈的方法虽然易于理解但效率较低。通过对不同方法的时间性能测试,得出遍历方式在处理大规模数据时表现最佳。
422 1
|
机器学习/深度学习 传感器 编解码
史上最全 | BEV感知算法综述(基于图像/Lidar/多模态数据的3D检测与分割任务)
以视觉为中心的俯视图(BEV)感知最近受到了广泛的关注,因其可以自然地呈现自然场景且对融合更友好。随着深度学习的快速发展,许多新颖的方法尝试解决以视觉为中心的BEV感知,但是目前还缺乏对该领域的综述类文章。本文对以视觉为中心的BEV感知及其扩展的方法进行了全面的综述调研,并提供了深入的分析和结果比较,进一步思考未来可能的研究方向。如下图所示,目前的工作可以根据视角变换分为两大类,即基于几何变换和基于网络变换。前者利用相机的物理原理,以可解释性的方式转换视图。后者则使用神经网络将透视图(PV)投影到BEV上。
史上最全 | BEV感知算法综述(基于图像/Lidar/多模态数据的3D检测与分割任务)
|
监控 数据可视化 安全
探究架构之 - 45张图玩转Kong Gateway,建议收藏系列 (一)
探究架构之 - 45张图玩转Kong Gateway,建议收藏系列 (一)
1399 1
探究架构之 - 45张图玩转Kong Gateway,建议收藏系列 (一)
|
网络协议 网络架构
IPv6基础知识
本文档详细介绍了IPv6协议的发展背景及其带来的主要变化,涵盖了IPv6数据报的基本首部和扩展首部结构,以及IPv6地址的表示方法和分类。由于IPv4地址资源有限且设计存在缺陷,IPv6应运而生,解决了这些问题并引入了许多新特性。文档还探讨了IPv6地址的不同类型,如单播、多播和任播地址,并讨论了IPv4向IPv6过渡的策略,包括双协议栈和隧道技术。
615 8
|
SQL NoSQL 关系型数据库
TX-LCN分布式事务(1)
TX-LCN分布式事务(1)
306 1