LinkedHashMap 是如何保证返回的顺序性的?

LinkedHashMap 源码阅读

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

先来看一下 LinkedHashMap 的继承关系,它继承了 HashMap,并且实现了 Map 接口。

LinkedHashMap 底层是 数组 + 链表 的形式,它通过双向的链表将插入进去的每个节点链接起来,以达到顺序返回的目的。

本篇内容主要关注 LinkedHashMap 是如何实现和维护这种链表结构的,以及 LinkedHashMap 的遍历是如何实现的。


在这里插入图片描述

上图展示的是 LinkedHashMap 中维护的大致结构,每个节点之间通过链相连,构成一个连续的双向链表,其中的节点是 LinkedHashMap 的静态内部类:

/**
     * HashMap.Node subclass for normal LinkedHashMap entries.
     */
    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);
        }
    }

这个类继承了 HashMap 中的 Node 类,在其基础上加了两个指针 before 和 after,分别指向它的后一个节点和前一个节点。

接下来具体来看一下,LinkedHashMap 中是如何维护链表结构的:

    public static void main(String[] args) {
        LinkedHashMap<Object, Object> hashMap = new LinkedHashMap<>();
        hashMap.put(1, 1);
        System.out.println(hashMap);
    }

上面展示的是一个简单的主方法,通过调试这段代码来阅读其 put() 方法的源码。

V put(K key, V value)

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

调用的仍然是 HashMap 的 put 方法,这个方法一定不陌生了,具体请看这篇文章:
万字源码解析!彻底搞懂 HashMap【二】:putVal 方法和 resize 方法(重点)

如果仅仅是调用了这个方法肯定是无法形成上面的双向链表的,那唯一的可能就是 LinkedHashMap 中重写了某些被 putVal() 调用的 HashMap 中的方法,通过调试可以很清楚的知道,重写的是 newNode() 方法

在这里插入图片描述

Node<K,V> newNode(int hash, K key, V value, Node<K,V> e)

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

方法中实例化了一个 LinkedHashMap.Entry 对象,然后调用了 linkNodeLast() 方法,就是通过这个方法将链表节点连接起来的

void linkNodeLast(LinkedHashMap.Entry<K,V> p)

// link at the end of list
    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;
        }
    }

首先获取了此时的 last 节点,也就是链表的最后一个元素,然后做了一个判断,如果 last 是空的,就说明此时数组中没有元素,是第一次添加,此时就将 head 也置为 p 节点,否则就将 p 与 last 节点双向连接。

读到这里其实就可以大致了解到 LinkedHashMap 的机制,就是将 HashMap 中的 Node 节点替换为了类中的 Entry,新增了 before 和 after 节点来创造出一个依据顺序连接的双向链表,由此来达到存储插入顺序的目的。

再来看一下删除的方法

V remove(Object key)

public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

同样,也是调用了父类 HashMap 中的 removeNode() 方法,然后重写其中调用的某个方法来达到维护链表的顺序。

HashMap 中的这个方法的实现也比较简单,这里就不赘述了,就是通过哈希映射到数组的某个索引,然后寻找需要删除的节点,下面展示的是其找到节点后执行删除逻辑的代码:

if (node != null && (!matchValue || (v = node.value) == value ||
                     (value != null && value.equals(v)))) {
    if (node instanceof TreeNode)
        ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
    else if (node == p) // 是第一个节点
        tab[index] = node.next;
    else // 是中间的某个节点
        p.next = node.next;
    ++modCount;
    --size;
    afterNodeRemoval(node);
    return node;
}

在 LinkedHashMap 中重写了 afterNodeRemoval() 方法去维护链表的顺序性,传入的 node 在本次 remove() 中被删除的节点,注意它实际上是 LinkedHashMap.Entry 实例,它还有指向链表前后元素的指针,所以并不会被清除。

void afterNodeRemoval(Node<K,V> 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;
    }

首先使用 p 将传入的节点保存下来(向下转型变为 LinkedHashMap.Entry<K,V>),使用 a 和 b 保存链表中节点的前后节点,然后就是对链表的维护,方式也比较简单,就是将前一个节点和后一个节点连接。

然后来看一下 LinkedHashMap 是如何遍历的,Map 集合的遍历是依赖于其中的 entrySet,可以通过 entrySet() 来获取这个 Set 集合,方法返回的是一个 Set<Map.Entry<K, V>> 类型的集合,其中 K 是键的类型,V 是值的类型。

通过 Set 集合可以利用迭代器或者增强 for 循环实现遍历,其中使用的迭代器为 LinkedEntryIterator

LinkedEntryIterator

    final class LinkedEntryIterator extends LinkedHashIterator
        implements Iterator<Map.Entry<K,V>> {
        public final Map.Entry<K,V> next() { return nextNode(); }
    }

这个类在 LinkedHashIterator 的基础上实现了 Iterator 接口,LinkedHashIterator 是一个公用的继承类,很多迭代器都通过继承它来实现一些方法

在这里插入图片描述

能保证返回顺序的关键是其中的 nextNode() 方法:

LinkedHashMap.Entry<K,V> 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 = e.after;
            return e;
        }

可以看到此时的 next 指向的是 e.after 即使用的是链表中维护的顺序,这样就能保证返回的顺序性。

  • 24
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

*Soo_Young*

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值