java集合 LinkedHashMap原理, 通过源码学习进行深入了解

概述

LinkedHashMap 继承自 HashMap,在 HashMap 基础上,通过维护一条双向链表,解决了 HashMap 不能随时保持遍历顺序和插入顺序一致的问题。除此之外,LinkedHashMap 对访问顺序也提供了相关支持。在一些场景下,该特性很有用,比如缓存。在实现上,LinkedHashMap 很多方法直接继承自 HashMap,仅为维护双向链表覆写了部分方法。所以,要看懂 LinkedHashMap 的源码,需要先看懂 HashMap 的源码。关于 HashMap 的源码分析参考:HashMap原理,通过源码学习进行深入了解

关键变量

除了子类的HashMap的成员变量外,LinkedHashMap因为需要维护一条双向链表,所以类需要多存储两个节点,即链表的头节点head和尾节点tail。
除此之外,因为LinkedHashMap 对访问顺序也提供了相关支持,需要设置一个标志accessOrder,代表不同的模式存储,当它为true时,开启访问模式,在LinkedHashMap调用到put、putIfAbsent、get、getOrDefault、compute、computeIfPresent、merge方法时,会将最先访问的数据放到末尾。具体在下面会介绍。

	private static final long serialVersionUID = 3801124242820219131L;

    /**
     * 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;
      /**
     * The iteration ordering method for this linked hash map: <tt>true</tt>
     * for access-order, <tt>false</tt> for insertion-order.
     *节点存储的顺序:
     *如果为 true 表示以访问(get、put)模式存储,最新访问的放在链表末尾
     * 如果为 false 表示以插入(put)模式存储,最近插入的放在链表末尾(注意此处的插入不包括修改)
     * @serial
     */
    final boolean accessOrder;

内部类

static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after;      //多了2个指向头尾节点的指针。
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }

关键方法

工具方法

 // internal utilities
    // 内部工具
    // 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;
        }
    }
    // apply src's links to dst
    // 将 src 节点替换成 dst 节点
    private void transferLinks(LinkedHashMap.Entry<K,V> src,
                               LinkedHashMap.Entry<K,V> dst) {
        LinkedHashMap.Entry<K,V> b = dst.before = src.before;
        LinkedHashMap.Entry<K,V> a = dst.after = src.after;
        if (b == null)
            head = dst;
        else
            b.after = dst;
        if (a == null)
            tail = dst;
        else
            a.before = dst;
    }

重写HashMap中的方法

newNode方法和replacement等方法

之所以LinkedHashMap底层也是拉链式存储,是因为该类直接使用了HashMap中的大部分方法,只是为了多维护一条双链表,LinkedHashMap将原本在HashMap的一些对于节点新建或操作的时候会调用的一些方法进行了重写。
具体如下:

   void reinitialize() {
        super.reinitialize();
        head = tail = null;
    }
    // 重写了hashmap中的方法,所以putVal中的newNode方法会被替换成调用这个版本
    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 节点,回收原来的节点
    Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
        LinkedHashMap.Entry<K,V> q = (LinkedHashMap.Entry<K,V>)p;
        LinkedHashMap.Entry<K,V> t =
                new LinkedHashMap.Entry<K,V>(q.hash, q.key, q.value, next);
        transferLinks(q, t);  //用t替换q。
        return t;
    }
    // 创建树节点
    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;
    }
     //将普通节点转化为红黑树节点,
    TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
        LinkedHashMap.Entry<K,V> q = (LinkedHashMap.Entry<K,V>)p;
        TreeNode<K,V> t = new TreeNode<K,V>(q.hash, q.key, q.value, next);
        transferLinks(q, t);
        return t;
    }

以上方法实质上,都是为了让HashMap的一些操作,也可以对维护的从head到tail的双向链表适用,即重写的目的就是Map在添加删除转化节点等操作时,同时维护节点间的双链表结构。


核心方法

在HashMap源码中有如下代码段:

 // Callbacks to allow LinkedHashMap post-actions
    //提供给linkedHashMap调用。
    void afterNodeAccess(Node<K,V> p) { }
    void afterNodeInsertion(boolean evict) { }
    void afterNodeRemoval(Node<K,V> p) { }
    

而这三个代码是LinkedHashMap的最重要的方法。

afterNodeRemoval方法

 // 节点删除之后的操作
    void afterNodeRemoval(Node<K,V> e) { // unlink
        LinkedHashMap.Entry<K,V> p =   //转化为LinkedHashMap节点,得到前后索引b和a
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        p.before = p.after = null;//方便GC
        if (b == null)   // 维护双向链表结构
            head = a;
        else
            b.after = a;
        if (a == null)
            tail = b;
        else
            a.before = b;
    }

afterNodeInsertion方法

当满足 removeEldestEntry(first)方法放回true的情况下,则要删除处于链表头部的节点,因为最近最新访问的会被放在链表尾部,或者插入模式中,最近最新插入的会在链表尾部。即LRU淘汰需要重写方法。

//插入后调用的维护双向链表结构
 // 移除最老的节点
    //evict为false表示处于构建hashmap的过程,true表示往创建好的Map中新增
    void afterNodeInsertion(boolean evict) { // possibly remove eldest
        LinkedHashMap.Entry<K,V> first;
        // 默认的 removeEldestEntry 永远返回 false,实现 LRU 需要重写此函数
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            K key = first.key;
            removeNode(hash(key), key, null, false, true);
        }
    }
/*
     *此方法通常不会以任何方式修改映射,而是允许映射根据其返回值的指示修改自身。
     * 允许此方法直接修改映射,但如果这样做,则必须返回<tt>false</tt>
     * (表示映射不应尝试进一步修改)。
     * 在此方法中修改映射后返回<tt>true</tt>的效果未指定
     * <p>This implementation merely returns <tt>false</tt> (so that this
     * map acts like a normal map - the eldest element is never removed).
     * 此方法在 put 和 putAll 里调用,用于删除最老的节点。
     * @param    eldest The least recently inserted entry in the map, or if
     *           this is an access-ordered map, the least recently accessed
     *           entry.  This is the entry that will be removed it this
     *           method returns <tt>true</tt>.  If the map was empty prior
     *           to the <tt>put</tt> or <tt>putAll</tt> invocation resulting
     *           in this invocation, this will be the entry that was just
     *           inserted; in other words, if the map contains a single
     *           entry, the eldest entry is also the newest.
     *      映射中最近插入的最旧的条目,
     *     或者如果这是按访问顺序排列的映射,
     *      则是最近访问最少的条目。如果此方法返回<tt>true</tt>,
     *      则将删除此项。如果映射在导致此调用的<tt>put</tt>或<tt>putAll</tt>调用之前为空,
     *     则这将是刚刚插入的条目;
     *       换句话说,如果映射包含单个条目,则最早的条目也是最新的条目
     * @return   <tt>true</tt> if the eldest entry should be removed
     *           from the map; <tt>false</tt> if it should be retained.
     */
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }

而其中removeEldesEntry就是LinkedHashMap实现缓存管理用到的方法,通常要重写该方法:
案例:最大容量为 100 的 LinkedHashMap:

private static final int MAX_ENTRIES = 100;

  protected boolean removeEldestEntry(Map.Entry eldest) {
    return size() > MAX_ENTRIES;
    }

案例中,当Map中数据操作100时,就会进行淘汰,从链表头部删除元素。


afterNodeAccess方法

如果accessOrder为true,则说明访问模式,这时此方法会在访问数据后生效,即将访问的节点移动到链表末尾,同时要维护双向链表的结构,以及存储的头部和尾部。

  // 访问节点之后调整链表结构
    // 将访问的节点移动到链表末尾
    void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
        // 如果 accessOrder 为 true 说明是访问模式,需要调整
        // 如果 accessOrder 为 false 说明是插入模式,不需要调整
        if (accessOrder && (last = tail) != e) {   //访问模式,并且这个值不在链表的末尾。
            LinkedHashMap.Entry<K,V> p =
                    (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            // p 当前节点
            // b 前一个节点
            // a 后一个节点
            p.after = null;   //放到末尾所以after为null
            if (b == null)    //如果传入的是头节点
                head = a;   //则移动后,头节点更新为传入节点的after
            else                //如果不是头节点
                b.after = a;   //则前面节点b的after要指向a.
            if (a != null)          //如果传入节点不是尾结点
                a.before = b;     //则后面节点a的before要指向b
            else            //如果是尾结点,
                last = b;     //last赋值为传入节点的前一个节点。
            if (last == null)   //如果b==null.a==nuall.链表为空。
                head = p;      //头节点指向p。
            else {
                p.before = last;
                last.after = p;
            }
            tail = p;    //更新链表尾部指向p。
            ++modCount;
        }
    }

总结

在日常开发中,LinkedHashMap 的使用频率虽不及 HashMap,但它也个重要的实现。在 Java 集合框架中,HashMap、LinkedHashMap 和 TreeMap 三个映射类基于不同的数据结构,并实现了不同的功能。HashMap 底层基于拉链式的散列结构,并在 JDK 1.8 中引入红黑树优化过长链表的问题。基于这样结构,HashMap 可提供高效的增删改查操作。LinkedHashMap 在其之上,通过维护一条双向链表,实现了散列数据结构的有序遍历。TreeMap 底层基于红黑树实现,利用红黑树的性质,实现了键值对排序功能。
不难看出 LinkedHashMap 就只是在 HashMap 之上添加了一个 LinkedList 而已,并没有构造新的节点或结构,而且维护双向链表的代码也非常简单。
LinkedHashMap 中比较重要的是 accessOrder 属性,它定义了双向链表中节点的存储序列,可用于实现 LRU 等淘汰算法。
关于LRU的案例可参考:LRU 和 LFU 缓存淘汰策略

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值