• Map容器家族(LinkedHashMap源码详解)


    一、在Map集合家族的位置及描述

                             

             LinkedHashMap是HashMap的子类,底层数据结构是链表和哈希表并且线程不安全,链表保证了遍历的有序性(元素插入的顺序)、哈希表保证了键的唯一性。实现了Map接口,内部还维护了一个双向链表,在每次插入数据,或者访问、修改数据时,会增加节点、或调整链表的节点顺序。

            默认情况下遍历时按照插入顺序输出,父类HashMap的遍历顺序是无序的。在构造器中可以传入accessOrder参数,是的其遍历顺序为访问顺序。

            因继承HashMap,除了输出无序,其他LinkedHashMap都有,比如扩容的策略,哈希桶长度一定是2的N次方等等。在实现时,就是重写override了几个方法。以满足其输出序列有序的需求。

    二、存储元素的数据结构

             LinkedHashMap的节点Entry<K, V>继承自HashMap.Node<K,V>,在其基础上扩展成了双向链表。

        /**
         * 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的基础上增加了三个成员变量head、tail、accessOrder

        /**
         * 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.
         *
         * @serial
         */
        final boolean accessOrder;  // 遍历顺序:true访问顺序、false插入顺序。默认插入顺序

    四、构造方法

            与HashMap相比,除了增加了一个参数accessOrder参数外,基本没有变化。

        // 利用一个Map构建
        public LinkedHashMap(Map<? extends K, ? extends V> m) {
            super();
            accessOrder = false;
            putMapEntries(m, false);    //该方法上文分析过,批量插入一个map中的所有数据到 本集合中。
        }
    
        // 指定初始化容量
        public LinkedHashMap(int initialCapacity) {
            super(initialCapacity);
            accessOrder = false;
        }
    
        // 指定初始化容量和加载因子
        public LinkedHashMap(int initialCapacity, float loadFactor) {
            super(initialCapacity, loadFactor);
            accessOrder = false;
        }
    
        // 指定初始化顺序、加载因子、遍历顺序顺序
        public LinkedHashMap(int initialCapacity,
                             float loadFactor,
                             boolean accessOrder) {
            super(initialCapacity, loadFactor);
            this.accessOrder = accessOrder;
        }
    
        // 利用一个Map构建
        public LinkedHashMap(Map<? extends K, ? extends V> m) {
            super();
            accessOrder = false;
            putMapEntries(m, false);    //该方法上文分析过,批量插入一个map中的所有数据到 本集合中。
        }

    五、常用API

    1.添加元素

            LinkedHashMap并没有重写任何put相关的方法,但其重写了构建节点的newNode方法;newNode方法会在putVal方法中被调用;putVal方法会在向集合中插入数据的时候被调用(单条插入put(K key, V value),批量插入putMapEntries(Map<? extends K, ? extends V> m, boolean evict))。LinkedHashMap重写了newNode方法,在每次构建新节点时,通过LinkedNodeLast方法将新节点链接在双向链表的尾部。

        // 构建的节点是LinkedHashMap重写的节点Entry
        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;
        }
    
        // link at the end of list
        // 链接到链表尾部
        private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
            LinkedHashMap.Entry<K,V> last = tail;   // 将尾指针保存到临时变量中
            tail = p;   // 新添加节点p作为尾节点
            if (last == null)   // 尾节点为空,则为空链表
                head = p;   // 将新添加节点p同时作为头结点
            else {  // 否则
                p.before = last;    // 将新添加节点前驱指向last
                last.after = p;     // 将last的后继指向p
            }                       // 即将p链接到链表中
        }

            在HashMap给子类LinkedHashMap预留了一些回调方法:afterNodeAccess()、afterNodeInsertion()、afterNodeRemoval()

    HashMap中预留的方法:

        // Callbacks to allow LinkedHashMap post-actions
        void afterNodeAccess(Node<K,V> p) { }
        void afterNodeInsertion(boolean evict) { }
        void afterNodeRemoval(Node<K,V> p) { }

    LinkedHashMap对父类预留的方法的实现:

        // 回调函数
        // 当使用get系列获取元素方法时被调用
        // 功能:移动节点到链表尾部,从而实现
        void afterNodeAccess(Node<K,V> e) { // move node to last
            LinkedHashMap.Entry<K,V> last;  // 临时节点,存放尾链表尾部节点
            if (accessOrder && (last = tail) != e) {    // accessOrder是true,且e不是尾节点
                LinkedHashMap.Entry<K,V> p =
                    (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after; // 获取p(e)前面的一个节点和后面的一个节点
                p.after = null; // 将p的后继指向空
                if (b == null)  // p的前驱为空,即p为头结点,移除头结p后将它的后继节点a作为头节点
                    head = a;
                else    // p的前驱不为空,则将p的前驱指向p的后继
                    b.after = a;
                if (a != null)  // a不为空,则指向b;为空则将尾指针指向b
                    a.before = b;
                else
                    last = b;
                if (last == null)   // 若last为空,则链表为空,头节点指向p
                    head = p;
                else {
                    p.before = last;// 否则链向链表尾部
                    last.after = p;
                }
                tail = p;   // 尾部指向p
                ++modCount;
            }
        }
    
        // 回调函数
        // 新节点插入之后回调 , 根据evict判断是否需要删除最老插入的节点。
        // 如果实现LruCache会用到这个方法。
        void afterNodeInsertion(boolean evict) { // possibly remove eldest
            LinkedHashMap.Entry<K,V> first;
            if (evict && (first = head) != null && removeEldestEntry(first)) {
                K key = first.key;
                removeNode(hash(key), key, null, false, true);
            }
        }
        //LinkedHashMap 默认返回false 则不删除节点。 
        // 返回true 代表要删除最早的节点。
        // 通常构建一个LruCache会在达到Cache的上限是返回true
        protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
            return false;
        }
    
        // 回调函数
        // 将节点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;  // 待删除节点 p 的前置后置节点都置空
            if (b == null)   // 如果前置节点是null,则现在的头结点应该是后置节点a
                head = a;
            else    // 否则将前置节点b的后置节点指向a
                b.after = a;
            if (a == null)  // 同理如果后置节点时null ,则尾节点应是b
                tail = b;
            else    // 否则更新后置节点a的前置节点为b
                a.before = b;
        }

            afterNodeInsertion(boolean evict)以及removeEldestEntry(Map.Entry<K,V> eldest)是构建LruCache需要的回调。

    2.删除元素

            LinkedHashMap也没有重写remove方法,因为它的删除逻辑和HashMap无区别。 但它重写了afterNodeRemoval()这个回调方法(HashMap中预留的方法之一)。该方法会在Node<K,V> removeNode(int hash, Object key, Object value, boolean matchValue, boolean movable)方法中被回调,removeNode()会在所有涉及到删除节点的方法中被调用,上文分析过,是删除节点操作的真正执行者。

        // 回调函数
        // 将节点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;  // 待删除节点 p 的前置后置节点都置空
            if (b == null)   // 如果前置节点是null,则现在的头结点应该是后置节点a
                head = a;
            else    // 否则将前置节点b的后置节点指向a
                b.after = a;
            if (a == null)  // 同理如果后置节点时null ,则尾节点应是b
                tail = b;
            else    // 否则更新后置节点a的前置节点为b
                a.before = b;
        }

    3.查询操作

            LinkedHashMap重写了查询方法,get(Object key)和getOrDefault(Object key, V defaultValue)方法。

        public V get(Object key) {
            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) {
           Node<K,V> e;
           if ((e = getNode(hash(key), key)) == null)
               return defaultValue;
           if (accessOrder)
               afterNodeAccess(e);
           return e.value;
       }

            对比HashMap中的实现,LinkedHashMap只是增加了在成员变量(构造函数时赋值)accessOrder为true的情况下,要去回调void afterNodeAccess(Node<K,V> e)函数。

            在afterNodeAccess()函数中,会将当前被访问到的节点e,移动至内部的双向链表的尾部。在函数中,会修改modCount,因此当你正在accessOrder=true的模式下,迭代LinkedHashMap时,如果同时查询访问数据,也会导致fail-fast,因为迭代的顺序已经改变。

        // 回调函数
        // 当使用get系列获取元素方法时被调用
        // 功能:移动节点到链表尾部,从而实现
        void afterNodeAccess(Node<K,V> e) { // move node to last
            LinkedHashMap.Entry<K,V> last;  // 临时节点,存放尾链表尾部节点
            if (accessOrder && (last = tail) != e) {    // accessOrder是true,且e不是尾节点
                LinkedHashMap.Entry<K,V> p =
                    (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after; // 获取p(e)前面的一个节点和后面的一个节点
                p.after = null; // 将p的后继指向空
                if (b == null)  // p的前驱为空,即p为头结点,移除头结p后将它的后继节点a作为头节点
                    head = a;
                else    // p的前驱不为空,则将p的前驱指向p的后继
                    b.after = a;
                if (a != null)  // a不为空,则指向b;为空则将尾指针指向b
                    a.before = b;
                else
                    last = b;
                if (last == null)   // 若last为空,则链表为空,头节点指向p
                    head = p;
                else {
                    p.before = last;// 否则链向链表尾部
                    last.after = p;
                }
                tail = p;   // 尾部指向p
                ++modCount;
            }
        }

    六、总结

            LinkedHashMap相对于HashMap的源码比,是很简单的。因为大树底下好乘凉。它继承了HashMap,仅重写了几个方法,以改变它迭代遍历时的顺序。这也是其与HashMap相比最大的不同。 在每次插入数据,或者访问、修改数据时,会增加节点、或调整链表的节点顺序。以决定迭代时输出的顺序。

           accessOrder ,默认是false,则迭代时输出的顺序是插入节点的顺序。若为true,则输出的顺序是按照访问节点的顺序。为true时,可以在这基础之上构建一个LruCache.
    LinkedHashMap并没有重写任何put方法。但是其重写了构建新节点的newNode()方法.在每次构建新节点时,将新节点链接在内部双向链表的尾部
            accessOrder=true的模式下,在afterNodeAccess()函数中,会将当前被访问到的节点e,移动至内部的双向链表的尾部。值得注意的是,afterNodeAccess()函数中,会修改modCount,因此当你正在accessOrder=true的模式下,迭代LinkedHashMap时,如果同时查询访问数据,也会导致fail-fast,因为迭代的顺序已经改变。
            该方法的实现可以看出,迭代LinkedHashMap,就是从内部维护的双链表的表头开始循环输出。 而双链表节点的顺序在LinkedHashMap的增、删、改、查时都会更新。以满足按照插入顺序输出,还是访问顺序输出。它与HashMap比,还有一个小小的优化,重写了containsValue()方法,直接遍历内部链表去比对value值是否相等。

  • 相关阅读:
    对java的Thread的理解
    Bugku的web题目(多次)的解题
    对网易云音乐参数(params,encSecKey)的分析
    并发编程知识的简单整理(二)
    并发编程知识的简单整理(一)
    用python代码编写的猜年龄小游戏
    python进阶与文件处理(数据类型分类,python深浅拷贝,异常处理,字符编码,基本文件操作,绝对路径和相对路径,,高级文件操作,文件的修改)
    计算机基础以及编程语言
    python基础-3(数据类型以及内置方法、解压缩、python与用户交互)
    python基础-2(格式化输出的三种方式,基本运算符,流程控制之if判断,流程控制之while循环,流程控制之for循环)
  • 原文地址:https://www.cnblogs.com/IdealSpring/p/11871186.html
Copyright © 2020-2023  润新知