Java8 LinkedHashMap 源码阅读

如果你对 HashMap 的源码有了解的话,只需要一图就能知道 LinkedHashMap 的原理了,但是具体的实现细节还是需要去读一下源码。

在这里插入图片描述

一、LinkedHashMap 简介

1.1 继承结构

从继承结构上来讲 LinkedHashMap 继承自 HashMapLinkedHashMap 中没有提供任何增删改查的方法,而是直接复用了父类 HashMap 中的方法。

在这里插入图片描述

1.2 内部数据结构

LinkedHashMap 继承自 HashMapHashMap 底层存储键值对的数据结构是 NodeTreeNode,而 LinkedHashMap 存储键值对的数据结构是 EntryTreeNode

    // Entry 继承自 HashMap 中的 Node 类,用于链表存储键值对
    static class Entry<K,V> extends HashMap.Node<K,V> {
        // 前置与后置节点,用于维护 LinkedHashMap 的键值对顺序
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }

Entry 用于存储链表中的键值对,用 beforenext 指针维护插入键值对的顺序,那树节点是怎么维护插入顺序的呢?在 LinkedHashMap 中是没有 TreeNode 类的,因为它复用了 HashMap 中的 TreeNode,下面我们来看一下 HashMapTreeNode 的定义。

我们只看这一行代码就行了,你会发现 HashMapTreeNode 类继承了 LinkedHashMapEntry,这样一来就继承了父类的前置与后置指针,也就能维护 LinkedHashMap 的插入顺序了。

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V>

1.3 构造函数

我们只看其中一个构造函数

    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        // 调用 HashMap 的构造函数
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

LinkedHashMap 只有这一个构造函数可以初始化 accessOrder,这个 accessOrder 属性又有什么作用呢,下面来解释一下,看了解释大家可能还会有点迷惑,后面结合代码大家就能理解这个字段的作用了。

    /*
     * 键值对迭代顺序策略
     * true:access-order     访问顺序,使用迭代器遍历时,get 的元素会被添加到最后
     * false:insertion-order 插入顺序
     */
    final boolean accessOrder;

上面讲了那么多都是为相关源码分析作准备,下面我们就一起来看看吧。

二、添加键值对

上面我们也说了 LinkedHashMap 中没有 put 方法,因为都是复用的 HashMap 的,如果你想具体了解 put 方法,可以查看 HashMap 扩容机制与线程安全分析 这篇文章。

想要把键值对放到 LinkedHashMap 中去,一定会先封装成 EntryTreeNode 节点,知道这个原理我们直接来看 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;
    }

newNode 方法初始化 Entry 节点时调用了一个 linkNodeLast 方法,我想看到这里大家应该这道其中的原理了。

    private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
        // 获取并记录 tail 节点
        LinkedHashMap.Entry<K,V> last = tail;
        // 重置 tail
        tail = p;
        if (last == null)
            head = p;
        else {
            // 将节点连接起来,构成双向链表
            p.before = last;
            last.after = p;
        }
    }

三、获取键值对

LinkedHashMapget 方法调用了 HashMapgetNode 方法,获取到对应的节点后,返回对应的 value。

    public V get(Object key) {
        Node<K,V> e;
        // 调用 hashMap 中的 getNode() 方法,根据 key 的哈希值找到对应的桶位置,判断节点后(链表、头结点、树节点)进行返回
        if ((e = getNode(hash(key), key)) == null)
            return null;
        // 如果 accessOrder 为 true,获取元素后把当前键值对调整到尾部
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
    }

下面我们来看一下 getNode 方法都做了什么。这个方法做的事情很简答, 根据 key 计算出对应的哈希值,根据哈希值计算出对应的桶位置,然后遍历节点进行查找。

    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        // 进行判断并通过 tab[(n - 1) & hash] 计算当前 key 在哈希表中的位置
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            // 如果是当前桶位置上的头节点直接返回
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            // 如果头节点不是要找的节点,判断是树节点还是链表节点后继续查找
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                // 链表、从头节点开始遍历查找
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

上面的 get 方法中还有一个需要注意的地方,如果 accessOrderatrue 则调用 fterNodeAccess(e); 方法,这个方法又是干嘛用的呢,在最开始的时候我们说过,LinkedHashMap 支持两种迭代策略,分别是访问顺序(默认)与插入顺序,这个 fterNodeAccess(e); 方法就是用于支持访问顺序的。

如果 accessOrderatrue,那么在调用 get 方法时会将该键值对置为双向链表的尾节点。

    void afterNodeAccess(Node<K,V> e) { // move node to last
        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;
            // 把当前节点的 after 节点置 null
            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;
        }
    }

四、HashMap 与 LinkedHashMap 对比

除了双向链表外,HashMapLinkedHashMap 还有什么区别吗?

4.1 containsValue 方法对比

HashMap 中代码如下,遍历哈希表中的所有桶,然后遍历桶位置上的所有节点。

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

LinkedHashMap 中代码如下,从头节点遍历,一直遍历到尾节点。

    public boolean containsValue(Object value) {
        /**
         * 遍历双向链表,判断 value,与 HashMap 完全不同
         */
        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;
    }

4.2 nextNode 方法对比

HashMap 中的 nextNode() 方法,遍历哈希表数组,接着遍历桶位置上的所有节点。

     final Node<K,V> nextNode() {
          // 记录哈希表数组
         Node<K,V>[] t;
         Node<K,V> e = next;
         if (modCount != expectedModCount)
             throw new ConcurrentModificationException();
         if (e == null)
              throw new NoSuchElementException();
          // 过滤掉没有键值对的桶位置
         if ((next = (current = e).next) == null && (t = table) != null) {
              do {} while (index < t.length && (next = t[index++]) == null);
         }
          // 下一个有键值对的桶(单个节点、树节点或链表)
         return e;
     }

LinkedHashMap 中的 nextNode() 方法,根据双向链表的顺序迭代键值对。

    final LinkedHashMap.Entry<K,V> nextNode() {
        LinkedHashMap.Entry<K,V> e = next;
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        if (e == null)
            throw new NoSuchElementException();
        current = e;
        // next 为双向链表的下一个节点
        next = e.after;
        return e;
    }

jdk1.8 源码阅读:https://github.com/zchen96/jdk1.8-source-code-read

参考资料

搞懂 Java LinkedHashMap 源码

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值