资讯专栏INFORMATION COLUMN

源码|jdk源码之LinkedHashMap分析

B0B0 / 2930人阅读

摘要:扩展的节点包括和,加入两个域组织额外的双向链表保存顺序。实现迭代器相关逻辑,因为迭代器是根据双向链表顺序迭代的。

HashMap作为一种经典的数据结构,其根据key定位元素能达到平均O(1)的时间复杂度。 但是,存储于HashMap中的元素显然是无序的,遍历HashMap的顺序得看脸。。。
那如何使得HashMap里的元素变得有序呢?一种思路是,将存放HashMap元素的节点,使用指针将他们串起来。换言之,就像在HashMap里面“嵌入”了一个链表一样。
实际上,jdk的LinkedHashMap就是使用这种思路实现的。

继承HashMap

LinkedHashMap中的代码不算多,这是因为,jdk的设计使用继承复用了代码,在jdk的设计中,LinkedHashMap是HashMap的扩展:

public class LinkedHashMap
    extends HashMap
    implements Map
{
    /* ... */
}
对节点进行扩展 父类HashMap中的节点

回想一下HashMap的实现方式中,将key和value打包成的节点有两种:
第一种,传统分离链表法的链表节点。

static class Node implements Map.Entry {
    /* ... */
}

第二种,HashMap为进行优化,一定情况下会将链表重构为红黑树。第二种节点是红黑树节点:

static final class TreeNode extends LinkedHashMap.Entry {
    /* ... */
}

突然发现,HashMap的TreeNode是继承至LinkedHashMap的Entry的。。。
个人观点是jdk这种做法不是很优雅,本身LinkedHashMap继承HashMap就使得两者之间的逻辑混在了一起,而这里的内部实现又反过来继承,逻辑搞得很混乱。

扩展节点

LinkedListHashMap需要将节点串成一个“嵌入式”双向链表,因此需要给这两种节点增加两个字段:

static class Entry extends HashMap.Node {
    Entry before, after;
    Entry(int hash, K key, V value, Node next) {
        super(hash, key, value, next);
    }
}

扩展HashMap.Node,增加双向链表字段。
由于TreeNode是继承自LinkedListMap.Entry的,所以它也有这两个字段。

属性

再来看下LinkedHashMap中的属性,很少:

/**
 * The head (eldest) of the doubly linked list.
 */
transient LinkedHashMap.Entry head;

/**
 * The tail (youngest) of the doubly linked list.
 */
transient LinkedHashMap.Entry tail;

记录双向链表的表头和表尾。从注释中可以看出,head节点是最老的,tail节点是最新的,也即链表按照由老到新的顺序串起来。

最后,由于LinkedHashMap是可以设置它组织元素的顺序。一种是链表中的元素是按插入时候的顺序排序,另外一种是按照访问的顺序排序。

// 指定顺序是按照访问顺序来,还是插入顺序来
final boolean accessOrder;

这个accessOrder指定是否按插入顺序来。

重写创建节点的函数

由于对Map中的节点进行了扩展,因此,在创建节点时不能使用原来的节点了,而应该使用重新创建后的。
HashMap将创建节点的操作抽取出来放到了多带带的函数中,LinkedHashMap重写即可:

    Node newNode(int hash, K key, V value, Node e) {
        LinkedHashMap.Entry p =
            new LinkedHashMap.Entry(hash, key, value, e);
        linkNodeLast(p);
        return p;
    }

    Node replacementNode(Node p, Node next) {
        LinkedHashMap.Entry q = (LinkedHashMap.Entry)p;
        LinkedHashMap.Entry t =
            new LinkedHashMap.Entry(q.hash, q.key, q.value, next);
        transferLinks(q, t);
        return t;
    }

    // HashMap的TreeNode是继承自LinkedHashMap.Entry的,因此能够参与组织双向链表
    TreeNode newTreeNode(int hash, K key, V value, Node next) {
        TreeNode p = new TreeNode(hash, key, value, next);
        linkNodeLast(p);
        return p;
    }

    TreeNode replacementTreeNode(Node p, Node next) {
        LinkedHashMap.Entry q = (LinkedHashMap.Entry)p;
        TreeNode t = new TreeNode(q.hash, q.key, q.value, next);
        transferLinks(q, t);
        return t;
    }
在获取、插入、删除元素时维护双向链表

接下来,则需要在LinkedHashMap的操作时维护双向链表。

删除

回顾下HashMap的源代码,我们知道,HashMap在删除节点后,会调用afterNodeRemoval函数。
这个函数在HashMap中是空的,实际上jdk是将它设计为一个hook,果然,在LinkedHashMap中,就重写了该函数,在其中维护双向链表:

// 当有节点被删除(即有元素被移除),那么也要将它从双向链表中移除
void afterNodeRemoval(Node e) { // unlink
    LinkedHashMap.Entry p =
        (LinkedHashMap.Entry)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;
}
插入

按照类似的思路,HashMap中在插入元素后会调用afterNodeInsertion,那是不是LinkedHashMap也在这里实现了相关逻辑,插入元素后维护双向链表节点呢?

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

然而,实际上在LinkedHashMap中该函数似乎没有什么用。因为:

protected boolean removeEldestEntry(Map.Entry eldest) {
    return false;
}

removeEldestEntry始终返回false,afterNodeInsertion相当于什么也没做。这个逻辑设计目的是什么,还不能很清楚。也许也是为了让谁去继承?以后再探究。

那插入元素后的在哪里维护了双向链表呢?回到之前的newNodenewTreeNode

Node newNode(int hash, K key, V value, Node e) {
    LinkedHashMap.Entry p =
        new LinkedHashMap.Entry(hash, key, value, e);
    linkNodeLast(p);
    return p;
}

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

// 尾插双向链表节点
private void linkNodeLast(LinkedHashMap.Entry p) {
    LinkedHashMap.Entry last = tail;
    tail = p;
    if (last == null)
        head = p;
    else {
        p.before = last;
        last.after = p;
    }
}

由于HashMap中调用newNode时候都是为了装新插入的元素,所以在这里维护双向链表。
感觉耦合是不是太紧了。。。如果HashMap由于某个操作需要临时搞个newNode借用下,岂不是会出问题?

下面是replacementNodereplacementTreeNode
replacementNode在HashMap中的作用是,该K V之前是被TreeNode包装的,现在需要拿Node包装它。这也势必会影响双向链表的结构,所以这里也需要额外维护下。

获取
获取的时候,同样,是重写了`afterNodeAccess`钩子,这样在HashMap的获取逻辑结束后,这里的逻辑会被执行,维护双向链表。
void afterNodeAccess(Node e) { // move node to last
    LinkedHashMap.Entry last;
    if (accessOrder && (last = tail) != e) {
        LinkedHashMap.Entry p =
            (LinkedHashMap.Entry)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中的顺序有访问序和插入序,只有访问序才需要在访问的时候更新双向链表结构。也即accessOrder为true才会执行这段逻辑。

最后,注意到:

++modCount;
}

一般来说,只有修改了Map结构的操作,才需要修改modCount以让正在迭代的迭代器感知到了变化。
但是这里,由于迭代器是使用这里的“嵌入式”双向链表进行迭代,而在这里会改变双向链表的结构,若迭代器继续迭代会造成不可预测的结果。
所以,这里需要改变modCount,阻止迭代器继续迭代。

典型应用场景

LinkedHashMap的一个典型应用场景是LRU算法。

由于现在夜已深,现在不敢熬夜身体吃不消,想睡觉了。所以这个坑以后再填

最后

LinkedHashMap还有其它的一些实现细节,如:

clear的时候也要同时维护双向链表;

根据双向链表实现迭代器。

最后,总结下jdk中对LinkedHashMap中的实现思路:

扩展HashMap实现。

扩展HashMap的节点(包括Node和TreeNode),加入两个域组织额外的双向链表保存顺序。

在产生插入、删除、访问的地方维护双向链表,通过重写某些方法实现。

实现迭代器相关逻辑,因为迭代器是根据双向链表顺序迭代的。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/77006.html

相关文章

  • 【java源码一带一路系列】LinkedHashMap.afterNodeAccess()

    摘要:如今行至于此,当观赏一方。由于所返回的无执行意义。源码阅读总体门槛相对而言比,毕竟大多数底层都由实现了。比心可通过这篇文章理解创建一个实例过程图工作原理往期线路回顾源码一带一路系列之源码一带一路系列之源码一带一路系列之 本文以jdk1.8中LinkedHashMap.afterNodeAccess()方法为切入点,分析其中难理解、有价值的源码片段(类似源码查看是ctrl+鼠标左键的过程...

    levy9527 评论0 收藏0
  • LinkedHashMap 源码详细分析JDK1.8)

    摘要:关于的源码分析,本文并不打算展开讲了。大家可以参考我之前的一篇文章源码详细分析。在删除节点时,父类的删除逻辑并不会修复所维护的双向链表,这不是它的职责。在节分析链表建立过程时,我故意忽略了部分源码分析。 1. 概述 LinkedHashMap 继承自 HashMap,在 HashMap 基础上,通过维护一条双向链表,解决了 HashMap 不能随时保持遍历顺序和插入顺序一致的问题。除此...

    Harriet666 评论0 收藏0
  • JDK源码(容器篇)

    摘要:三系列用于保存键值对,无论是,还是已弃用的或者线程安全的等,都是基于红黑树。是完全基于红黑树的,并在此基础上实现了接口。可以看到,只有红黑树,且红黑树是通过内部类来实现的。 JDK容器 前言 阅读JDK源码有段时间了,准备以博客的形式记录下来,也方便复习时查阅,本文参考JDK1.8源码。 一、Collection Collection是所有容器的基类,定义了一些基础方法。List、Se...

    Soarkey 评论0 收藏0
  • java源码

    摘要:集合源码解析回归基础,集合源码解析系列,持续更新和源码分析与是两个常用的操作字符串的类。这里我们从源码看下不同状态都是怎么处理的。 Java 集合深入理解:ArrayList 回归基础,Java 集合深入理解系列,持续更新~ JVM 源码分析之 System.currentTimeMillis 及 nanoTime 原理详解 JVM 源码分析之 System.currentTimeMi...

    Freeman 评论0 收藏0
  • 这几道Java集合框架面试题在面试中几乎必问

    摘要:若遇到哈希冲突,则将冲突的值加到链表中即可。之后相比于之前的版本,之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值默认为时,将链表转化为红黑树,以减少搜索时间。有序,唯一红黑树自平衡的排序二叉树。 本文是最最最常见Java面试题总结系列第三周的文章。主要内容: Arraylist 与 LinkedList 异同 ArrayList 与 Vector 区别 HashMap的底层...

    bigdevil_s 评论0 收藏0

发表评论

0条评论

B0B0

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<