LinkedHashMap源码解析

前言

HashMap 提供的访问,是无序的。而在一些业务场景下,我们希望能够提供有序访问的 HashMap 。那么此时,我们就有两种选择:
TreeMap :按照 key 的顺序。
LinkedHashMap :按照 key 的插入和访问的顺序。

LinkedHashMap ,在 HashMap 的基础之上,提供了顺序访问的特性。而这里的顺序,包括两种:

而LinkedHashMap比HashMap优于以下几点
LinkedHashMap 内部维护了一个双向链表,解决了 HashMap 不能随时保持遍历顺序和插入顺序一致的问题
LinkedHashMap 元素的访问顺序也提供了相关支持,也就是我们常说的 LRU(最近最少使用)原则。

类图

在这里插入图片描述

实现 Map 接口。
继承 HashMap 类。

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

}

before 属性,指向前一个节点。after 属性,指向后一个节点。
通过 before + after 属性,我们就可以形成一个以 Entry 为节点的链表

既然 LinkedHashMap 是 LinkedList + HashMap 的组合

/**
 * 头节点。
 *
 * 越老的节点,放在越前面。所以头节点,指向链表的开头
 *
 * The head (eldest) of the doubly linked list.
 */
transient LinkedHashMap.Entry<K,V> head;

/**
 * 尾节点
 *
 * 越新的节点,放在越后面。所以尾节点,指向链表的结尾
 *
 * The tail (youngest) of the doubly linked list.
 */
transient LinkedHashMap.Entry<K,V> tail;

/**
 * 是否按照访问的顺序。
 *
 * true :按照 key-value 的访问顺序进行访问。
 * false :按照 key-value 的插入顺序进行访问。
 *
 * The iteration ordering method for this linked hash map: {@code true}
 * for access-order, {@code false} for insertion-order.
 *
 * @serial
 */
final boolean accessOrder;

仔细看下每个属性的注释。
head + tail 属性,形成 LinkedHashMap 的双向链表。而访问的顺序,就是 head => tail 的过程。
accessOrder 属性,决定了 LinkedHashMap 的顺序。也就是说:
true 时,当 Entry 节点被访问时,放置到链表的结尾,被 tail 指向。
false 时,当 Entry 节点被添加时,放置到链表的结尾,被 tail 指向。如果插入的 key 对应的 Entry 节点已经存在,也会被放到结尾

构造方法

LinkedHashMap 一共有 5 个构造方法,其中四个和 HashMap 相同,只是多初始化 accessOrder = false 。所以,默认使用插入顺序进行访问。

另外一个 #LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) 构造方法,允许自定义 accessOrder 属性

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

值得注意的是 LinkedHashMap 并没有覆写任何关于 HashMap put 方法。所以调用 LinkedHashMap 的 put 方法实际上调用了父类 HashMap 的方法。

创建节点

在插入 key-value 键值对时,例如说 #put(K key, V value) 方法,如果不存在对应的节点,则会调用 #newNode(int hash, K key, V value, Node<K,V> e) 方法,创建节点。
因为 LinkedHashMap 自定义了 Entry 节点,所以必然需要重写该方法。代码如下:

Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    // <1> 创建 Entry 节点
    LinkedHashMap.Entry<K,V> p =
        new LinkedHashMap.Entry<>(hash, key, value, e);
    // <2> 添加到结尾
    linkNodeLast(p);
    // 返回
    return p;
}

<1> 处,创建 Entry 节点。虽然此处传入 e 作为 Entry.next 属性,指向下一个节点。但是实际上,#put(K key, V value) 方法中,传入的 e = null 。
<2> 处,调用 #linkNodeLast(LinkedHashMap.Entry<K,V> p) 方法,添加到结尾。代码如下:

private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    // 记录原尾节点到 last 中
    LinkedHashMap.Entry<K,V> last = tail;
    // 设置 tail 指向 p ,变更新的尾节点
    tail = p;
    // 如果原尾节点 last 为空,说明 head 也为空,所以 head 也指向 p
    if (last == null)
        head = p;
    // last <=> p ,相互指向
    else {
        p.before = last;
        last.after = p;
    }
}

三个重要的回调函数

在 linkindHashMap 的读取、添加、删除时,分别提供了 #afterNodeAccess(Node<K,V> e)、#afterNodeInsertion(boolean evict)、#afterNodeRemoval(Node<K,V> e) 回调方法。这样,LinkedHashMap 可以通过它们实现自定义拓展逻辑。

void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }

afterNodeAccess

在 accessOrder 属性为 true 时,当 Entry 节点被访问时,放置到链表的结尾,被 tail 指向。所以 #afterNodeAccess(Node<K,V> e) 方法的代码如下:

void afterNodeAccess(Node<K,V> e) { // move node to last
    LinkedHashMap.Entry<K,V> last;
    // accessOrder 判断必须是满足按访问顺序。
    // (last = tail) != e 将 tail 赋值给 last ,并且判断是否 e 已经是队尾。如果是队尾,就不用处理了。
    if (accessOrder && (last = tail) != e) {
        // 将 e 赋值给 p 【因为要 Node 类型转换成 Entry 类型】
        // 同时 b、a 分别是 e 的前后节点
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        // 第一步,将 p 从链表中移除
        p.after = null;
        // 处理 b 的下一个节点指向 a
        if (b == null)
            head = a;
        else
            b.after = a;
        // 处理 a 的前一个节点指向 b
        if (a != null)
            a.before = b;
        else
            last = b;
        // 第二步,将 p 添加到链表的尾巴。实际这里的代码,和 linkNodeLast 是一致的。
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
        // tail 指向 p ,实际就是 e 。
        tail = p;
        // 增加修改次数
        ++modCount;
    }
}

链表的操作看起来比较繁琐,实际一共分成两步:1)第一步,将 p 从链表中移除;2)将 p 添加到链表的尾巴。

因为 HashMap 提供的 #get(Object key) 和 #getOrDefault(Object key, V defaultValue) 方法,并未调用 #afterNodeAccess(Node<K,V> e) 方法,这在按照读取顺序访问显然不行,所以 LinkedHashMap 重写这两方法的代码,如下:

public V get(Object key) {
    // 获得 key 对应的 Node
    Node<K,V> e;
    if ((e = getNode(hash(key), key)) == null)
        return null;
    // 如果访问到,回调节点被访问
    if (accessOrder)
        afterNodeAccess(e);
    return e.value;
}

public V getOrDefault(Object key, V defaultValue) {
    // 获得 key 对应的 Node
    Node<K,V> e;
   if ((e = getNode(hash(key), key)) == null)
       return defaultValue;
    // 如果访问到,回调节点被访问
    if (accessOrder)
       afterNodeAccess(e);
   return e.value;
}

afterNodeInsertion

在开始看 #afterNodeInsertion(boolean evict) 方法之前,我们先来看看如何基于 LinkedHashMap 实现 LRU 算法的缓存。代码如下

class LRUCache<K, V> extends LinkedHashMap<K, V> {

    private final int CACHE_SIZE;

    /**
     * 传递进来最多能缓存多少数据
     *
     * @param cacheSize 缓存大小
     */
    public LRUCache(int cacheSize) {
        // true 表示让 LinkedHashMap 按照访问顺序来进行排序,最近访问的放在头部,最老访问的放在尾部。
        super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true);
        CACHE_SIZE = cacheSize;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        // 当 map 中的数据量大于指定的缓存个数的时候,就自动删除最老的数据。
        return size() > CACHE_SIZE;
    }

}

我们在 #afterNodeInsertion(boolean evict) 方法中来理解。代码如下:

// LinkedHashMap.java

// evict 翻译为驱逐,表示是否允许移除元素
void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMap.Entry<K,V> first;
    // first = head 记录当前头节点。因为移除从头开始,最老
    // <1> removeEldestEntry(first) 判断是否满足移除最老节点
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        // <2> 移除指定节点
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}

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

默认情况下,都不移除最老的节点。所以在上述的 LRU 缓存的示例,重写了该方法,判断 LinkedHashMap 是否超过缓存最大大小。如果是,则移除最老的节点。
如果满足条件,则调用 #removeNode(…) 方法,移除最老的节点。

afterNodeRemoval

在节点被移除时,LinkedHashMap 需要将节点也从链表中移除,所以重写 #afterNodeRemoval(Node<K,V> e) 方法,实现该逻辑。代码如下

void afterNodeRemoval(Node<K,V> e) { // unlink
    // 将 e 赋值给 p 【因为要 Node 类型转换成 Entry 类型】
    // 同时 b、a 分别是 e 的前后节点
    LinkedHashMap.Entry<K,V> p =
        (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
    // 将 p 从链表中移除
    p.before = p.after = null;
    // 处理 b 的下一个节点指向 a
    if (b == null)
        head = a;
    else
        b.after = a;
    // 处理 a 的前一个节点指向 b
    if (a == null)
        tail = b;
    else
        a.before = b;
}

总结

LinkedHashMap 是 HashMap 的子类,增加了顺序访问的特性。
【默认】当 accessOrder = false 时,按照 key-value 的插入顺序进行访问。
当 accessOrder = true 时,按照 key-value 的读取顺序进行访问。
LinkedHashMap 的顺序特性,通过内部的双向链表实现,所以我们把它看成是 LinkedList + LinkedHashMap 的组合。
LinkedHashMap 通过重写 HashMap 提供的回调方法,从而实现其对顺序的特性的处理。同时,因为 LinkedHashMap 的顺序特性,需要重写 #keysToArray(T[] a) 等遍历相关的方法。
LinkedHashMap 可以方便实现 LRU 算法的缓存,

LinkedHashMap 拥有与 HashMap 相同的底层哈希表结构,即数组 + 单链表 + 红黑树,也拥有相同的扩容机制。
LinkedHashMap 相比 HashMap 的拉链式存储结构,内部额外通过 Entry 维护了一个双向链表。
HashMap 元素的遍历顺序不一定与元素的插入顺序相同,而 LinkedHashMap 则通过遍历双向链表来获取元素,所以遍历顺序在一定条件下等于插入顺序。
LinkedHashMap 可以通过构造参数 accessOrder 来指定双向链表是否在元素被访问后改变其在双向链表中的位置。
LinkedHashMap和HashMap都是线程不安全的。
LinkedHashMap的结构是数组+链表(+红黑树)+双向链表。双向链表是用来维护元素的顺序的。HashMap的元素是无序的,LinkedHashMap的元素是有序的。LinkedHashMap的存取数据的方式还是跟HashMap一致
和HashMap的不同之处有:遍历方式不一样(扩容是用到遍历),HashMap根据按数组顺序来遍历,如果遇到链表,则依次遍历链表中的元素,完了之后继续遍历数组的元素。而LinkedHashMap是按双向列表来遍历的,所以LinkedHashMap遍历的性能比HashMap高一点,因为HashMap中有空元素。

看图就知道为什么要看成LinkedList + LinkedHashMap 的组合。
LinkedList + LinkedHashMap 的组合。

上图中,淡蓝色的箭头表示前驱引用,红色箭头表示后继引用。每当有新键值对节点插入,新节点最终会接在 tail 引用指向的节点后面。而 tail 引用则会移动到新的节点上,这样一个双向链表就建立起来了
黑色表示next指针

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
LinkedHashMap 是 Java 中的一种特殊类型的 HashMap,它保留了插入顺序,即按照元素插入的先后顺序进行排序。 根据提供的引用内容,在 LinkedHashMap 之前还有其他几种集合类的介绍。因此,无法直接确定 LinkedHashMap 取前十个元素的具体方法。 如果您想获取 LinkedHashMap 中的前十个元素,可以使用以下方法: 1. 创建一个 LinkedHashMap 对象,并添加需要的元素; 2. 使用 LinkedHashMap 的 entrySet() 方法,获取所有键值对的 Set 集合; 3. 将 entrySet 转换为 List,以便进行排序; 4. 使用 Collections 类的 sort() 方法对 List 进行排序,按照插入顺序进行排序; 5. 使用 subList() 方法截取前十个元素。 以下是示例代码: ``` LinkedHashMap<String, Integer> linkedHashMap = new LinkedHashMap<>(); // 添加元素到 LinkedHashMap 中 List<Map.Entry<String, Integer>> entryList = new ArrayList<>(linkedHashMap.entrySet()); Collections.sort(entryList, new Comparator<Map.Entry<String, Integer>>() { @Override public int compare(Map.Entry<String, Integer> entry1, Map.Entry<String, Integer> entry2) { return 0; // 使用插入顺序进行排序,保留原始顺序 } }); List<Map.Entry<String, Integer>> firstTenEntries = entryList.subList(0, Math.min(10, entryList.size())); ``` 通过以上代码,您可以获取到 LinkedHashMap 中的前十个元素。请注意,如果 LinkedHashMap 中的元素不足十个,则将返回所有元素。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [【集合码系列】LinkedHashMap解析](https://blog.csdn.net/weixin_43934607/article/details/108871825)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [java集合-LinkedHashMap的使用](https://download.csdn.net/download/qq_39725872/87906595)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [【集合】LinkedHashMap](https://blog.csdn.net/weixin_44175122/article/details/116739916)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值