• LinkedHashMap源码解析-Java8


    目录

    一.介绍

      1.1 HashMap无法保证顺序

      1.2 如何保证HashMap的顺序

      1.3 使用LinkedHashMap

      1.4 LinkedHashMap的顺序分类

      1.5 LinkedHashMap使用示例

    二.LinkedHashMap源码分析

      2.1 LinkedHashMap原理概览

      2.2 链表的节点类型

      2.3 新增的属性

      2.4 构造方法

      2.5 LinkedHashMap的put

      2.6 some post-actions

        2.6.1 afterNodeAccess

        2.6.2 afterNodeInsertion

        2.6.3 afterNodeRemoval

        2.6.3 internalNodeWrit

      2.7 LinkedHashMap的get

    三. 总结

      

      

    一.介绍

    1.1 HashMap无法保证顺序

      对于HashMap而言,将数据放入其中后,如果我们需要遍历map中的所有元素,可以这么做:

        1.通过map.entrySet()来获取包含所有数据的set,对该set进行遍历;

        2.通过map.keySet()获取所有的key,然后遍历key,调用get方法获取value;

      上面这两种方式都是可以进行map元素的遍历,但是同时也存在一个问题:数据的顺序无法保证,也就是说,在打印出所有元素前,元素的顺序是未知的。

    1.2 如何保证HashMap的顺序

      因为HashMap不能保证顺序,就需要借助其他的数据结构,比如可以额外使用一个List(比如ArrayList),每次加入map后,同时再加入ArrayList,如果需要顺序遍历数据,则直接遍历ArrayList即可,就不用遍历map了。

      但是这样的话,开销就会相对较高,首先需要创建ArrayList,还需要每次增删元素后,都要对ArrayList进行操作(移动元素),时间复杂度O(n),空间复杂度O(n)。

      

    1.3 使用LinkedHashMap

      上面使用ArrayList来保证数据的顺序性,当元素发生变化时,就需要移动ArrayList中的元素,时间复杂度O(n),空间复杂度为O(n),那么如果可以降低时间复杂度和空间复杂度就好了。

      于是,就会联想到链表,不考虑查找元素的开销,增删元素的时间复杂度为O(1);但是还是需要O(n)的空间复杂度,因为需要保存节点值,如果能把空间复杂度降下来就好了,此时就可以了解一下LinkedHashMap。

      LinkedHashMap,其实从名字上就可以看出他是功能,Linked+HashMap,说到Linked,自然就会联系到LinkedList,所以,LinkedhashMap可以简单的理解为LinkedList+HashMap。

      需要注意的是,LinkedHashMap并没有完全创建一个新的节点类型,而是在HashMap的Node内部类上进行扩充,增加前后指针,这样也是可以节省很多空间的。

    1.4 LinkedHashMap的顺序分类

      LinkedHashMap可以保证HashMap元素的顺序,这个顺序,有两种:

      1.遍历元素的顺序与插入元素的顺序一致,也就是说,依次插入A->B->C,那么遍历时的顺序就是A->B->C;

      2.初始时,遍历元素的顺序与插入的顺序一致,但是当元素被范文,也就是说,插入A->B->C后,遍历的顺序是A->B->C;如果中间访问访问或者修改过B,那么顺序就会变为A->C->B,

      在LinkedHashMap中,有一个accessOrder属性来设定是否按照访问顺序来遍历,默认为false,也就是上面默认使用第一种方式。

    1.5 LinkedHashMap使用示例

    package cn.ganlixin.util;
    import org.junit.Test;
    import java.util.LinkedHashMap;
    import java.util.Map;
    
    public class TestLinkedHashMap {
    
        /**
         * 遍历顺序和插入顺序相同,期间访问或者修改元素,顺序不会发生改变
         */
        @Test
        public void testNormal() {
            Map<String, String> map = new LinkedHashMap<>();
    
            map.put("one", "1111");
            map.put("two", "2222");
            map.put("three", "3333");
    
            for (Map.Entry entry : map.entrySet()) {
                System.out.println(entry.getKey() + "=>" + entry.getValue());
            }
            /** 输出如下:
             one=>1111
             two=>2222
             three=>3333
             */
    
            map.put("two", "2");
            for (Map.Entry entry : map.entrySet()) {
                System.out.println(entry.getKey() + "=>" + entry.getValue());
            }
            /** 输出如下:
             one=>1111
             two=>2222
             three=>3333
             */
        }
    
        /**
         * 遍历顺序为访问(修改)顺序,元素被访问或者被修改后,会移到最后
         */
        @Test
        public void testAccessOrder() {
            // public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)
            Map<String, String> map = new LinkedHashMap<>(16, 0.75f, true);
            // 创建map时,设置遍历顺序按照访问顺序
    
            map.put("one", "1111");
            map.put("two", "2222");
            map.put("three", "3333");
    
            for (Map.Entry entry : map.entrySet()) {
                System.out.println(entry.getKey() + "=>" + entry.getValue());
            }
            /**
             one=>1111
             two=>2222
             three=>3333
             */
    
            map.get("one"); // 某个元素访问后,会将该元素移动到末尾
            for (Map.Entry entry : map.entrySet()) {
                System.out.println(entry.getKey() + "=>" + entry.getValue());
            }
            /**
             two=>2222
             three=>3333
             one=>1111
             */
    
            map.put("two", "2"); // 修改元素后,也会将元素移动到最后
            for (Map.Entry entry : map.entrySet()) {
                System.out.println(entry.getKey() + "=>" + entry.getValue());
            }
            /**
             three=>3333
             one=>1111
             two=>2
             */
    
            map.replace("three", "3"); // 修改元素后,也会将元素移动到最后
            for (Map.Entry entry : map.entrySet()) {
                System.out.println(entry.getKey() + "=>" + entry.getValue());
            }
            /**
             three=>3333
             one=>1111
             two=>2
             */
        }
    }
    

      

    二.LinkedHashMap源码解析

    2.1 LinkedHashMap原理概览

      在阅读LinkedHashMap前,要先了解HashMap的原理,可以参考:HashMap源码解析-Java8,方便后面对LinkedHashMap源码的理解。

      LinkedHashMap继承自HashMap,也就是说HashMap的功能,LinkedHashMap都支持。

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

      LinkedHashMap对HashMap的部分API进行了重写,以此来保证map中元素遍历时的顺序。通过第一部分的一些介绍,其实我们大概才出LinkedHashMap是如何保证顺序的:

      1.对于遍历顺序与插入顺序相同的情况,只需要在将元素put到双链表后,维护节点的指针,链入上一次put的节点后面(成为尾结点);

      2.对于遍历顺序与访问顺序相同一致的情况,只需要在get、put、replace..操作之后,将节点移动到末尾即可。

    2.2 双链表的节点

      LinkedHashMap中双链表的节点类型,是直接在HashMap的节点类型上进行扩充的,增加了before和after指针;

    /**
     * 链表的节点类型Entry,继承自HashMap的Node内部类
     */
    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);
        }
    }
    

      

    2.3 新增属性

      LinkedHashMap继承自HashMap,也继承了HashMap中的所有属性,比如默认的初始容量、默认的负载因子、默认转换为红黑树和链表的阈值...

      除此之前,LinkedHashMap增加了3个额外的属性,其中两个属性用于实现双链表的头尾节点,另外一个属性用于控制顺序的类型:

    /**
     * 指向双链表的头结点(如果map为空=>双链表为空=>head为空)
     */
    transient LinkedHashMap.Entry<K, V> head;
    
    /**
     * 指向双链表的尾结点(如果map为空=>双链表为空=>tail为空)
     */
    transient LinkedHashMap.Entry<K, V> tail;
    
    /**
     * 设置LinkedHashMap的顺序类型
     * false:表示遍历元素的顺序与插入顺序相同
     * true:表示遍历元素的顺序按照访问的顺序排列,当一个数据被访问(修改)后,该数据就会移动到最后
     */
    final boolean accessOrder;
    

      

    2.4构造方法

      LinkedHashMap的构造方法,其实主要有两个点:1.设置HashMap的初始容量和负载因子;2.设置顺序类型(accessOrder)。  

    /**
     * 创建LinkedHashMap,使用HashMap默认的初始容量16,默认的负载因子0.75
     */
    public LinkedHashMap() {
        super();
        accessOrder = false;
    }
    
    /**
     * 创建LinkedHashMap,指定HashMap的初始容量,使用默认的负载因子0.75
     */
    public LinkedHashMap(int initialCapacity) {
        // 指定HashMap的初始容量
        super(initialCapacity);
        accessOrder = false;
    }
    
    /**
     * 创建LinkedHashMap,指定HashMap的初始容量和负载因子
     */
    public LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }
    
    /**
     * 创建LinkedHashMap,指定HashMap初始容量和负载因子,以及是否顺序获取
     */
    public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }
    
    /**
     * 创建LinkedHashMap,使用HashMap默认的初始容量(16)和负载因子(0.75)
     * 并且将传入的map放入到LinkedHashMap中
     */
    public LinkedHashMap(Map<? extends K, ? extends V> m) {
        super();
        accessOrder = false;
        putMapEntries(m, false);
    }
    

      

    2.5 LinkedHashMap的put

      LinkedHashMap并没有覆盖HashMap的put方法,而是直接沿用HashMap的put方法。

      那么LinkedHashMap是如何保证顺序的呢?

      是这样的,HashMap在put的时候:

      1.如果是新增节点,那么就会创建一个节点,然后放入到map中;

      2.如果put操作时修改操作(也就是put的key已经存在),那么不需要创建节点,只需要修改已有节点的value即可;

      对于第一种情况,创建节点,是HashMap和LinkedHashMap都有的,LinkedHashMap重写了HashMap创建新节点的方法(newNode和newTreeNode两个方法),在这个时候将新创建的节点加入链表的末尾:

    /**
     * HashMap的newNode,创建一个Node节点
     */
    Node<K, V> newNode(int hash, K key, V value, Node<K, V> next) {
        return new Node<>(hash, key, value, next);
    }
    
    /**
     * HashMap中的newTreeNode,创建一个红黑树的节点
     */
    TreeNode<K, V> newTreeNode(int hash, K key, V value, Node<K, V> next) {
        return new TreeNode<>(hash, key, value, next);
    }
    
    /**
     * LinkedHashMap重写HashMap的newNode方法,
     * 创建一个双链表节点,同时将新节点作为双链表的尾结点
     */
    @Override
    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重写HashMap的newTreeNode方法
     * 创建一个红黑树的节点,并将节点加入到双链表的最后
     */
    @Override
    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;
    }
    
    /**
     * 将节点加入链表中(挂在最后位置)
     */
    private void linkNodeLast(LinkedHashMap.Entry<K, V> p) {
        // tail指向的尾结点
        LinkedHashMap.Entry<K, V> last = tail;
    
        tail = p;
        if (last == null) {
            // last==null,证明插入节点前链表为空,此时head也需要指向p
            head = p;
    
        } else {
            // 插入节点前,链表不为空,维护指针关系,将p插入到最后
            p.before = last;
            last.after = p;
        }
    }

       对于put方法,进行了替换操作,就需要用到后面说的一些post-actions(后置处理)来完成顺序的保证。

    2.6 some post-actions

      由于LinkedHashMap是继承自HashMap,并且大部分的代码都没有做更改,也就是直接沿用HashMap的接口。那么LinkedHashMap是如何保证顺序的呢?特别是前面说的保证插入的顺序,保证访问的顺序??

      其实HashMap的一些API中,比如put方法,其中就包含了一些callback,当插入元素或者查找到元素后,就调用某个方法;这些callback方法在HashMap中没有进行任何操作(方法体为空),留给子类LinkedHashMap进行实现的,如下面所示:

    // Callbacks to allow LinkedHashMap post-actions
    
    /**
     * 当节点被访问后调用,比如
     *
     * @param p 被访问的节点
     */
    void afterNodeAccess(Node<K, V> p) {
    }
    
    /**
     * 当元素添加到map后,执行的操作
     * @param evict 
     */
    void afterNodeInsertion(boolean evict) {
    }
    
    /**
     * 当元素被移除后,执行的操作
     * @param p 被移除的节点
     */
    void afterNodeRemoval(Node<K, V> p) {
    }
    

      

    2.6.1 afterNodeAccess

      afterNodeAccess方法,是在元素节点被访问之后被调用,主要是以下几种(一部分):

      1.get操作

      2.put操作,如果是替换,那么就会先查找是否存在已有节点,若发现已经存在该节点(则该节点被访问),就会回调afterNodeAccess方法;

      3.遍历操作

      在LinkedHashMap中afterNodeAccess,主要执行的就是将元素移动到链表的最后,前提是accessOrder为true(在访问元素后,就将元素移动到链表移动到最后)。

    /**
     * accessOrder为true时,表示map中元素保持和访问的顺序相同
     * 所以需要将访问的节点移动到双链表的末尾
     *
     * @param e 包含元素最新值的Node节点
     */
    void afterNodeAccess(Node<K, V> e) { // move node to last
        LinkedHashMap.Entry<K, V> last;
    
        // accessOrder为true,表示map中数据的顺序按照访问顺序排序
        // 如果e不是最后一个元素才进行操作(因为
        if (accessOrder && (last = tail) != e) {
    
            // p指向e,b指向e的前继节点,a指向e的后继节点
            LinkedHashMap.Entry<K, V> p = (LinkedHashMap.Entry<K, V>) e, b = p.before, a = p.after;
    
            // 将p的后继设为null(因为要将其设为尾结点,尾结点的after为null)
            p.after = null;
    
            if (b == null) {
                // e的前继节点为null,证明e为头结点,则将e的后继节点设置为新头结点
                head = a;
            } else {
                // e不是头结点,则将e的后继节点设置为前继节点的后继节点
                b.after = a;
            }
    
            if (a != null) {
                // e节点不是尾结点,那么就将e的前继节点设置为后继节点的前继节点
                a.before = b;
            } else {
                // e的后继节点为null,证明e为尾结点,则将e的前继节点设置为尾结点
                last = b;
            }
    
            if (last == null) {
                // 如果修改指针关系(删除e后),last为null,也就是尾结点为null,证明链表为空了
                // 此时将p节点(也就是e节点)作为头结点
                head = p;
            } else {
    
                // 链表不为null,则将e节点挂到最后
                p.before = last;
                last.after = p;
            }
    
            // tail指针指向e节点
            tail = p;
    
            // 修改次数加1
            ++modCount;
        }
    }
    

      

    2.6.2 afterNodeInsertion

      afterNodeInsertion方法,是在HashMap的put和putAll方法调用后执行的,主要做的就是删除链表的头结点。

      在LinkedHashMap中,默认是不会每次删除头结点的,如果需要加入节点后进行删除头结点,则需要另外创建子类进行实现逻辑。

      上面提到一个eldestEntry,也就是最老的Entry,其实也就是链表的头结点(accessOrder=false->最早插入,accessOrder=true->最久未访问)。

    2.6.3 afterNodeRemoval

      当调用LinkedHashMap的remove接口,其实也就是调用HashMap的remove接口(LinkedHashMap没有重写该接口)。

      在HashMap中移除元素后,会回调afterNodeRemoval方法,该方法在HashMap中并做任何操作,而是留给子类LinkedHashMap进行定义对应的操作,LinkedHashMap做的就是维护链表指针,将删除的元素从链表中删除。

    /**
     * LinkedHashMap删除元素后,维护链表间的指针关系,将节点从双链表中删除
     */
    @Override
    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) {
            // b为e的前继节点,如果前继节点为null,证明e为头结点,则将第二个节点(e的后继)作为新的头结点
            head = a;
        } else {
            // e不是头结点,维护前继和后继的关系
            b.after = a;
        }
    
        if (a == null) {
            // 后继节点为空,表示e为尾结点,此时将e的前继节点b作为尾结点
            tail = b;
        } else {
            // e不是尾结点,则前继和后继的关系
            a.before = b;
        }
    }
    

      

    2.6.4 internalWriteEntries

      当序列化map的时候(使用ObjectOutputStream),会调用HashMap中的writeObject方法

    /**
     * 调用ObjectOutputStream来序列化map时,会调用writeObject方法
     */
    private void writeObject(java.io.ObjectOutputStream s) throws IOException {
        int buckets = capacity();
        
        // 将一些属性写入到流中
        s.defaultWriteObject();
        s.writeInt(buckets);
        s.writeInt(size);
    
        // 将数据写入
        internalWriteEntries(s);
    }
    
    /**
     * HashMap中的实现,将map中的数据顺序写入
     */
    void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
        Node<K, V>[] tab;
        // 如果map不为空,则遍历数组的每个位置进行操作
        if (size > 0 && (tab = table) != null) {
            for (int i = 0; i < tab.length; ++i) {
    
                // 每个位置可能是链表或者红黑树结构,则进行遍历时写入
                for (Node<K, V> e = tab[i]; e != null; e = e.next) {
                    s.writeObject(e.key);
                    s.writeObject(e.value);
                }
            }
        }
    }

      internalWriteEntries方法,就是在输出的时候决定数据的顺序,可以看到上面是HashMap的internalWriteEntries方法,是依次遍历数组的每个位置,然后遍历每个位置上的链表或者红黑树,这个时候顺序并没有什么规律。

      而LinkedHashMap,只是重写了internalWriteEntries方法,在其中对顺序进行调整:

    /**
     * LinkedHashMap中的实现
     */
    void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
        // 遍历双链表,从头到尾进行遍历,保证了顺序
        for (LinkedHashMap.Entry<K, V> e = head; e != null; e = e.after) {
            s.writeObject(e.key);
            s.writeObject(e.value);
        }
    }
    

    2.7 LinkedHashMap.get方法

    /**
     * 获取key对应的值
     */
    public V get(Object key) {
        Node<K, V> e;
    
        // 调用HashMap中的getNode
        if ((e = getNode(hash(key), key)) == null) {
            return null;
        }
    
        // 如果设置的顺序是按照访问顺序,那么就需要在访问节点后,将节点移到末尾
        if (accessOrder) {
            afterNodeAccess(e);
        }
    
        return e.value;
    }
    

      

    三.总结

      其实对于LinkedHashMap,没有太多的需要阐述的,更多的应该是看HashMap,因为维护节点顺序的操作都是在HashMap中进行回调的。

      简单理解LinkedHashMap,也就是利用双链表数据结构,在HashMap的Node节点类型上增加前后指针,每次访问或者修改节点后,会回调相关的callback,进行节点顺序的维护。节点的顺序可以分为插入顺序和访问顺序,通过accessOrder进行控制。

    四.常见的面试题

      一般是问LinkedHashMap的原理,比如:

      1.节点类型

      2.底层是双链表还是单链表

      3.节点的顺序(accessOrder)

      4.几个扩展点(afterNodeAccess、afterNodeInsertion、afterNodeRemoval)

      而关于节点的顺序,如果时间长了,可能会忘记,这里简单记一下:

      对于accessOrder为false,也就是默认情况下,遍历时元素的顺序只与插入的顺序有关;如果某个key对应的数据已经存在,再次put、replace该key后,元素的顺序不会发生改变。

      对于accessOrder为true,将元素加入map后:

        1.在没有访问的情况下,遍历map元素,顺序与插入顺序一致;

        2.访问元素(access),包括get、put、replace操作,无论是替换还是新增,元素都将移动到最后!!

      原文地址:https://www.cnblogs.com/-beyond/p/13412164.html

  • 相关阅读:
    段错误诊断!
    kissthank
    c实现面向对象编程(3)
    字符串转换成数字以及注意事项
    【Java并发编程实战】-----“J.U.C”:CLH队列锁
    c编程:僵尸吃大脑
    展示C代码覆盖率的gcovr工具简单介绍及相关命令使用演示样例
    最优化学习笔记(三)最速下降法
    maven学习
    ASP.NET MVC 入门4、Controller与Action
  • 原文地址:https://www.cnblogs.com/-beyond/p/13412164.html
Copyright © 2020-2023  润新知