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的有序分两种,一是根据每个节点的访问顺序实现的,二是根据每个节点的插入顺序(默认)实现的
  • 线程不安全




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

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

相关文章
|
5月前
HashMap源码
HashMap源码
|
6月前
|
存储 设计模式 Java
|
6月前
|
Go C语言 C#
HashMap中putMapEntries()方法源码详解
HashMap中putMapEntries()方法源码详解
|
存储 Java
LinkedHashMap源码解析
上文提到多次LRUCache,其中LRU是Least Recently Used的缩写,其实就是想在有限的存储空间里保留更多有价值数据的一种方式。其实现的依旧就是最佳被使用的数据将来还被使用的概率更高,这个现象在计算机领域非常明显,很多优化就是基于此的。LRUCache就是这样一种存储的实现,它的难点就在于如何高效地剔除掉最旧的数据,以及如何维护数据的新旧度。
52 0
|
存储 Java
Java集合学习:LinkedList源码详解
Java集合学习:LinkedList源码详解
146 0
|
存储 算法 容器
数据结构算法 - HashMap 源码解析
数据结构算法 - HashMap 源码解析
153 2
数据结构算法 - HashMap 源码解析
|
安全 Java
HashMap源码学习
线程上:HashMap是线程不安全的,而HashTable是安全的。key、value的支持:HashMap的key、balue可以为空,而HashTable是key、value不可以为空的。底层数据结构:HashMap采用数组+链表+红黑树,当链表的长度>=8的时候会考虑是否转成红黑树,而HashTable则没有。初始化容量上:HashTable的初始化容量是11,同时扩容变为原来的2n+1,HashMap的初始化容量是16,同时扩容扩容成原来的2倍。而当给定初始化容量时,HashTable是直接给定初始化容量,而HashMap是将给定的初始化容量变成2的次幂。
70 0
HashMap源码学习
|
存储 缓存
LinkedHashMap源码简读
1、LinkedHashMap继承自HashMap,HashMap具有的特性它都具有。 2、实际上,LinkedHashMap是通过双向链表和散列表这两种数据组合实现的。LinkedHashMap中的“Linked”实际上指的是双向链表,并非指“用链表法解决散列冲突”。 3、LinkedHashMap不仅支持按照插入顺序遍历数据,还支持按照访问顺序来遍历数据。通过设置`accessOrder`属性为true即可。也就是说它本身就是一个支持LRU缓存淘汰策略的缓存系统。
|
存储 Java 容器
HashMap 1.8 源码简读
HashMap 1.8 源码简读
|
存储
LinkedHashMap源码解析(上)
LinkedHashMap维护插入的顺序。
173 0
LinkedHashMap源码解析(上)