美文网首页
LinkedHashMap详解

LinkedHashMap详解

作者: 星空怎样 | 来源:发表于2020-05-29 10:26 被阅读0次

    [toc]

    前言

    本篇文章介绍容器类的另一个哈希表LinkedHashMap,这是HashMap的关门弟子,直接继承了HashMap的衣钵,拥有HashMap的全部特性,并且有着HashMap没有的特性,下面就介绍有多大能耐。

    LinkedHashMap简介与简单使用

    先看看LinkedHashMap的继承结构:

    image

    LinkedHashMap属于Map大家族的一员,直接继承了HashMap,所以有着HashMap的全部特性,如高效查找元素,同时,LinkedHashMap保持了内部元素的顺序,有两种顺序模式,保持元素插入顺序/访问顺序,因此适用于保持元素顺序的场景,另外由于LinkedHashMap保持元素访问顺序,所以常被用作LRU缓存(即最近最少使用缓存策略)

    LinkedHashMap的结构以及HashMap的对比

    LinkedHashMap中的结构比HashMap更加复杂,首先他有着HashMap相同的结构,元素用数组+链表+红黑树形式存储,除此之外,所有元素还使用了双链表进行连接,相当于HashMap和LinkedList两种结构的结合体,也正是因为元素之间使用了链表连接,所以才能让其中的元素可以保持某种顺序,但也增加了维护这个链表的卡爱笑,每次进行增加和删除元素时,除了需要调整HashMap的结构,还需要调整链表结构,不过链表调整的开销并不大,除了数据量特别大场景,一般可以小到忽略不计。另一方面,由于所有元素使用链表相连,所以遍历的效率略高于HashMap,因为HashMap遍历时,需要每个桶中先遍历到链表尾部,然后在遍历到下一个桶,当元素不多而桶数量很多时,就会有很多次无效访问,所以理论上来说,LinkedHashMap的遍历效率要比HashMap高的,说了这么多,好像LinkedHashMap处理要比HashMap好,为啥不都用LinkedHashMap呢?

    这个问题就跟LinkedList和ArrayList对比一样,两者都有适用的场景,并没有绝对的优势,所以还需要分情况分析。

    LinkedHashMap中的节点

    image
    image
    谜一样的继承关系,看完这个图,会想贵圈真乱,父类的TreeNode继承子类的Entry,子类中Entry又继承了父类中的Node,先别慌,这样做自然有他的道理,先来回顾一下Node结构,Node是HashMap中普通节点类,内部是这样的:
    static class Node<K,V> implements Map.Entry<K,V> {
            final int hash;
            final K key;
            V value;
            /**
             * 指向下一个节点的引用
             */
            Node<K,V> next;
            ......省略部分代码  
        }
    

    最上面的那个Entry是Map接口中的一个内部接口,只是规定了Entry该有的方法,并没有成员变量,在LinkedHashMap中,Entry内部类是这样的:

    static class Entry<K,V> extends Node<K,V> {
            Entry<K,V> before, after;
            Entry(int hash, K key, V value, Node<K,V> next) {
                super(hash, key, value, next);
            }
        }
    

    增加了before和after引用,这样便可以和其他Entry一起组成一个双向链表,节点不仅存储了键值对信息,还可以是用next,before,after来链接前后节点next引用仅用HashMap结构中链接同一个桶中的后一个元素而before和after引用则是用来链接LinkedHashMap中所有元素,将其链接成一个双向链表形式,接下来看根HashMap结构对比就很清晰了:

    • HashMap结构图
    image
    • LinkedHashMap结构图
      image

    回到之前问题,为什么TreeNode要继承自LinkdeHashMap中的Entry引用而不是直接继承Node呢,毕竟在HashMap中并不需要使用这样的链表性质,增加after和before两个引用只会浪费空间,原因如下:

    • 首先自然是为了复用代码,如果TreeNode直接继承Nnode,则失去了链表的特性,LinkedHashMap继承HashMap后,需要在使用拎一个内部类来继承TreeNode来使得其具有链表特性,并且相关方法均需要重写,大大增加工作量,并且代码的可维护性会下降很多。
    • 其次,HashMap只会在桶中元素达到一定数量时候才会将节点转换为TreeNode,在哈希表散列良好的情况下,TreeNode很少被使用,所以这一点浪费是值的的。

    LinkedHashMap可以看成由两部分组成,一部分与HashMap结构完全一致,另一部分则是一个双向链表,由于LinkedHashMap是直接继承自HashMap的,所以哈希表的维护结构就在HashMap中代码里进行,在LinkedHashMap中仅进行双链表的维护,这样也能很好的划分职能,使得代码结构更加清晰。

    两个哈希表插入同样元素,使用同样的变量方式进行遍历,HashMap的得到的是无序结构,而LinedHashMap得到的是跟插入顺序一致的结构,而产生这样的区别的根源就在于LinkedHashMap中的那个双向链表。

    LinkdeHashMap的插入和删除

    如果没看过源码,会想LinkedHashMap应该是通过重写put和remove方法来实现的,但事实上LinkedHashMap并没有覆盖插入和删除方法,这一点可以通过观察LinkedHashMap代码结构发现。

    put方法

    既然没有覆盖put方法,调用LinkdeHashMap中put方法为什么会跟HashMap中put方法得到结构不一样呢?让我们看一下put方法。

    //插入新的值,主要调用putVal方法
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
    //插入新值核心函数
    //如果参数onlyIfAbsent是true,那么不会覆盖相同key的值value
    //如果evict是false表示是在初始化时候调用的此函数
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //首先检查table是否为null并且容量是否大于0,即有没有初始化table
        //如果没有初始化就进行resize
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
         //先定位到桶的位置,p为该桶的头结点
         //如果p为null则说明该桶还没有节点,直接将新的键值对存入桶中 
        if ((p = tab[i = (n - 1) & hash]) == null)
            //newNode方法被LinkdeHashMap重写了
            tab[i] = newNode(hash, key, value, null);
         //p不为null,即发生了hash碰撞,进一步处理   
        else {
            Node<K,V> e; K k;
            //比较头结点的扰动值,及key的值
            //如果相同则说明存入接地昂key已经存在,而且就是头结点
            //先获取该接地昂,是否覆盖其值进一步处理
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
             //头结点的key和插入的key不相同
            //先判断桶内数据结构是否是红黑树,如果是则以红黑树的方式插入到树中   
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            //桶内节点不是红黑树,即链表结构    
            else {
                //循环遍历链表
                //直到找到与插入节点key相同的节点,如果没有找到直接插入到尾结点
                for (int binCount = 0; ; ++binCount) {
                    //已经遍历到尾结点,说明插入的key不存在,直接插入到尾部
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        //如果桶内节点数量达到了树型化阈值,则进行树型化,
                        //因为binCount从0开始所以TREEIFY_THRESHOLD-1
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    //插入的key已存在,先获取该节点,是否覆盖其值进一步处理
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            //如果获取到节点不为null则进行操作
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                //方法出入的onlyIfAbsennt参数为false,获取旧值为null则直接替换掉旧值
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                //LinkdeHashMap的关键方法,HashMap中是空方法
                afterNodeAccess(e);
                return oldValue;
            }
        }
        //以上操作及全部完成,并且已经成功插入或者更改一个节点的值
        //修改modCount的值,记录修改次数
        ++modCount;
        //更新size,并且判断是否超过阈值则进行扩容
        if (++size > threshold)
            resize();
        //LinkdeHashMap的关键方法,HashMap中是空方法 
        afterNodeInsertion(evict);
        return null;
    }
    
    //HashMap的newNode方法
    Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
            return new Node<>(hash, key, value, next);
        }
    //LinkdeHashMap的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;
        }
    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;
            }
        }
    

    在put方法中调用putVal方法时,其中tab[i] = newNode(hash, key, value, null);被LinkedHashMap覆盖了,还有两个值的注意的方法:

    //元素访问之后调用
    afterNodeAccess(e);
    //元素被插入之后调用
    afterNodeInsertion(evict);
    

    这个两个方法分别在元素被访问之后和元素被插入之后调用,而这两个回调方法,在HashMap中没有具体实现,只有一个空壳,留给LinkdeHashMap来覆盖,以下是HashMap中的定义:

      // Callbacks to allow LinkedHashMap post-actions
        //节点访问后
        void afterNodeAccess(Node<K,V> p) { }
        //节点插入后
        void afterNodeInsertion(boolean evict) { }
        //节点删除后
        void afterNodeRemoval(Node<K,V> p) { }
    

    以下是LinkdeHashMap的实现这几个方法的实现:

    //节点删除后的操作
    void afterNodeRemoval(Node<K,V> e) { // unlink
            //将e转为LinkedHashMap.Entry,获取他的上一个节点b,下一个节点a
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
            //将p的上下节点都释放掉
            p.before = p.after = null;
            //如果上一个节点是空,那么说明p节点是头结点,去掉之后a节点变为链表头部,否则就是b节点的下一个指向a节点
            if (b == null)
                head = a;
            else
                b.after = a;
            //判断尾部的情况
            if (a == null)
                tail = b;
            else
                a.before = b;
        }
        //节点插入后进行双链表的调整,插入节点会判断是否需要移除链表头节点,默认实现是不移除,可以通过继承LinkdeHashMap并覆盖removeEldesEntry改变这个特性
        void afterNodeInsertion(boolean evict) { // possibly remove eldest
            LinkedHashMap.Entry<K,V> first;
            //removeEldestEntry()方法默认是false
            //意思是是否删掉最旧的元素,头节点是最旧的元素,如果在缓存时候使用可以被用到
            if (evict && (first = head) != null && removeEldestEntry(first)) {
                K key = first.key;
                removeNode(hash(key), key, null, false, true);
            }
        }
        //在节点被访问之后进行双链表的调整(仅在accessOrder为true时候进行,把当前元素移动到尾部)
        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;
                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;
            }
        }
    

    现在在梳理一下逻辑,插入节点的时候,会调用覆盖过的newNode方法,将插入的元素连接到链表尾部,删除节点的时候,改节点的前后节点相连即可,当节点被访问时,可以将其放到链表尾部。

    删除remove

    以下是HashMap的删除方法

    public V remove(Object key) {
            Node<K,V> e;
            return (e = removeNode(hash(key), key, null, false, true)) == null ?
                null : e.value;
        }
       
        final Node<K,V> removeNode(int hash, Object key, Object value,
                                   boolean matchValue, boolean movable) {
            Node<K,V>[] tab; Node<K,V> p; int n, index;
            if ((tab = table) != null && (n = tab.length) > 0 &&
                (p = tab[index = (n - 1) & hash]) != null) {
                Node<K,V> node = null, e; K k; V v;
                if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                    node = p;
                else if ((e = p.next) != null) {
                    if (p instanceof TreeNode)
                        node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
                    else {
                        do {
                            if (e.hash == hash &&
                                ((k = e.key) == key ||
                                 (key != null && key.equals(k)))) {
                                node = e;
                                break;
                            }
                            p = e;
                        } while ((e = e.next) != null);
                    }
                }
                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;
                    //LinkedHashMap使用
                    afterNodeRemoval(node);
                    return node;
                }
            }
            return null;
        }
    

    其中afterNodeRemoval(上面已经有对该方法的注释)方法就是LinkdeHashMap使用会进行双向链表节点的移动

    LinkdeHashMap的构造方法

        //构造一个空的保持插入顺序的LinkedHashMap实例,使用指定的initialCapacity和loadFactor
        public LinkedHashMap(int initialCapacity, float loadFactor) {
            super(initialCapacity, loadFactor);
            accessOrder = false;
        }
    
        //构造一个空的保持插入顺序的LinkedHashMap实例,使用指定容量,默认负载因子(0.75)
        public LinkedHashMap(int initialCapacity) {
            super(initialCapacity);
            accessOrder = false;
        }
    
        //构造一个空的保持插入顺序的LinkedHashMap,使用默认容量16和默认负载因子0.75
        public LinkedHashMap() {
            super();
            accessOrder = false;
        }
    
        //使用指定Map的所有映射构造一个保持插入顺序的LinkedHashMap实例,使用默认的装载因子和可以容纳指定map中所有映射的合适的容量。
        public LinkedHashMap(Map<? extends K, ? extends V> m) {
            super();
            accessOrder = false;
            putMapEntries(m, false);
        }
    
        //使用指定的容量负载因子和指定顺序(true是访问顺序,false是插入顺序)来创建一个空的LinkedMap实例
        public LinkedHashMap(int initialCapacity,
                             float loadFactor,
                             boolean accessOrder) {
            super(initialCapacity, loadFactor);
            this.accessOrder = accessOrder;
        }
    

    LinkdeHashMap与HashMap的区别

    LinkedHashMap是HashMap的子类,他们最大的区别是,HashMap的迭代是无序的,而LinkedHashMap是有序的,并且有按照插入顺序和访问顺序两种方式(默认是按照插入顺序),为了实现有序的迭代,LinkedHashMap相比HashMap,额外维护了一个双向链表。

    相关文章

      网友评论

          本文标题:LinkedHashMap详解

          本文链接:https://www.haomeiwen.com/subject/ozqxzhtx.html