美文网首页
09-LinkedHashMap 核心源码分析(集合)

09-LinkedHashMap 核心源码分析(集合)

作者: xinxisimple | 来源:发表于2020-03-03 20:54 被阅读0次

    注:源码系列文章主要是对某付费专栏的总结记录。如有侵权,请联系删除。

    1 LinkedHashMap 整体架构

    HashMap 是无序的,TreeMap 可以按照 key 进行排序,那有木有 Map 是可以维护插入的顺序的呢?接下来我们看看 LinkedHashMap。

    LinkedHashMap 本身是继承 HashMap 的,所以它拥有 HashMap 的所有特性,在此基础上,还提供了两大特性:

    • 按照插入顺序进行访问;
    • 实现了访问最小最先删除功能,其目的是把很久都没有访问的 key 自动删除。

    1.1 按照插入顺序访问

    1.1.1 LinkedHashMap 链表结构

    // 链表头
    transient LinkedHashMap.Entry<K,V> head;
    
    // 链表尾
    transient LinkedHashMap.Entry<K,V> tail;
    
    // 继承 Node,为数组的每个元素增加了 before 和 after 属性
    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);
        }
    }
    
    // 控制两种访问模式的字段,默认 false
    // true: 按照访问顺序,会把经常访问的 key 放到队尾
    // false: 按照插入顺序提供访问
    final boolean accessOrder;
    

    从新增的属性可以看到,LinkedHashMap 的数据结构很像是把 LinkedList 每个元素换成了 HashMap 的 Node,像是两者的结合体,也正是因为增加了这些结构,从而能把 Map 的元素都串联起来,形成一个链表,而链表就可以保证顺序了,就可以维护元素插入进来的顺序。

    1.1.2 按照顺序新增

    LinkedHashMap 初始化时,默认的 accessOrder 是 false,就是会按照插入顺序提供访问,插入方法使用的是父类 HashMap 的 put 方法,不过覆写了 put 方法执行中调用的 newNode/newTreeNode 和 afterNodeAccess 方法。

    newNode/newTreeNode 方法,控制新增节点追加到链表的尾部,这样每次新节点都追加到尾部,即可保证插入顺序了,我们以 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;
    }
    
    // link at the end of list
    private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
        // 缓存旧的尾节点
        LinkedHashMap.Entry<K,V> last = tail;
        // 赋值尾节点为新增节点
        tail = p;
        // 如果旧的尾节点为null则表示当前链表为空,直接将新增节点赋值于头节点
        if (last == null)
            head = p;
        // 链表有数据,将旧的尾节点和新的尾节点互联
        else {
            p.before = last;
            last.after = p;
        }
    }
    

    LinkedHashMap 通过新增头节点、尾节点,给每个节点增加 before、after 属性,每次新增时,都把节点追加到尾节点等手段,在新增的时候,就已经维护了按照插入顺序的链表结构了。

    1.1.3 按照顺序访问

    LinkedHashMap 只提供了单向访问,即按照插入的顺序从头到尾进行访问,不能像 LinkedList 那样可以双向访问。

    我们主要通过迭代器进行访问,迭代器初始化的时候,默认从头节点开始访问,在迭代的过程中,不断访问当前节点的 after 节点即可。

    Map 对 key、value 和 entity(节点)都提供了迭代的方法,假设women携带 entity,就可以使用 LinkedHashMap.entrySet().iterator() 这种写法直接返回 LinkedHashIterator,LinkedHashIterator 是迭代器,我们通过调用迭代器的 nextNode() 方法就可以得到下一个节点,迭代器的源码如下:

    abstract class LinkedHashIterator {
        LinkedHashMap.Entry<K,V> next;
        LinkedHashMap.Entry<K,V> current;
        int expectedModCount;
    
        // 初始化时,默认从头节点开始访问
        LinkedHashIterator() {
            next = head;
            expectedModCount = modCount;
            current = null;
        }
    
        public final boolean hasNext() {
            return next != null;
        }
    
        final LinkedHashMap.Entry<K,V> nextNode() {
            // 当前遍历的节点
            LinkedHashMap.Entry<K,V> e = next;
            // 判断 modCount
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if (e == null)
                throw new NoSuchElementException();
            current = e;
            // 通过链表的 after 结构,找到下一个迭代的节点
            next = e.after;
            return e;
        }
    }
    

    在新增节点时,我们就已经维护了元素之间的插入顺序了,所以迭代访问是非常简答的,只需要不断的访问当前节点的下一个节点即可。

    1.2 访问最少删除策略

    1.2.1 演示

    这种策略也叫做 <a href="https://baike.baidu.com/item/LRU/1269842?fr=aladdin">LRU</a>(least recently used,最近最少使用),大概的意思就是经常访问的元素会被追加到队尾,这样不经常访问的数据自然就靠近队头,然后我们可以设置删除策略,比如当 Map 元素个数大于多少时,把头节点删除,如下:

    @Test
    public void accessOrderTest() {
        // 使用带参构造函数,分别指定初始化大小,加载因子,访问模式(accessOrder)
        LinkedHashMap<Integer, Integer> map = new LinkedHashMap<Integer, Integer>(5, 0.75f, true) {
            {
                put(1, 1);
                put(2, 2);
                put(3, 3);
                put(4, 4);
                put(5, 5);
            }
    
            // 覆写了删除策略方法,我们设定当节点个数大于 4 时,就开始删除头节点
            @Override
            protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
                return size() > 4;
            }
        };
    
        System.out.println("初始化: " + JSON.toJSONString(map));
        map.get(3);
        System.out.println("map.get(3): " + JSON.toJSONString(map));
        map.get(2);
        System.out.println("map.get(2): " + JSON.toJSONString(map));
    }
    

    执行结果:

    初始化: {2:2,3:3,4:4,5:5}
    map.get(3): {2:2,4:4,5:5,3:3}
    map.get(2): {4:4,5:5,3:3,2:2}
    

    可以看到,map 初始化的时候,我们放进去五个元素,但结果只有四个元素, 1 不见了,这个主要是因为我们覆写了 removeEldestEntry 方法,我们实现了如果 map 中元素个数大于 4 时,则就把队头的元素删除,当 put(5,5) 执行的时候,正好把队头的 1 删除,这个体现了当达到我们设定的删除策略时,会自动删除头节点。

    当我们调用 map.get(3) 方法时,元素 3 移动到了队尾,调用 map.get(2) 方法时,元素 2 被移动到了队尾,这个体现了经常被访问的节点会被移动到队尾。

    这个例子就很好的说明了访问最少删除策略,接下来看看原理。

    1.2.2 元素被转移到队尾

    为什么 get 时,元素会被移动到队尾:

    public V get(Object key) {
        Node<K,V> e;
        // 调用 HashMap get 方法
        if ((e = getNode(hash(key), key)) == null)
            return null;
        // 如果设置了 LRU 策略
        if (accessOrder)
            // 这个方法把当前 key 移动到队尾
            afterNodeAccess(e);
        return e.value;
    }
    

    从上述源码中,可以看到,通过 afterNodeAccess 方法把当前访问节点移动到了队尾,其实不仅仅是 get 方法,执行 getOrDefaultcomputecomputeIfAbsentcomputeIfPresentmerge 方法时,也会这么做,通过不断的把经常访问的节点移动到队尾,那么靠近队头的节点,自然就是很少被访问的元素了。

    1.2.3 删除策略

    上述例子中我们在执行 put 方法时,发现队头元素被删除了,LinkedHashMap 本身是没有 put 方法实现的,调用的是 HashMap 的 put 方法,但 LinkedHashMap 实现了 put 方法中的调用 afterNodeInsertion 方法,这个方法实现了删除,源码如下:

    // 删除很少被访问的元素,被 HashMap 的 put 方法所调用
    void afterNodeInsertion(boolean evict) { // possibly remove eldest
        // 得到链表的头节点
        LinkedHashMap.Entry<K,V> first;
        // 如果队列不为空,并且删除策略允许的情况下. removeEldestEntry 为我们覆写的删除策略
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            // 得到头节点的 key
            K key = first.key;
            // 删除头节点
            removeNode(hash(key), key, null, false, true);
        }
    }
    

    1.3 小结

    LinkedHashMap 提供了两个很有意思的功能:按照插入顺序访问和删除最少访问元素策略,简单的通过链表的结构就实现了,设计的非常巧妙。

    LinkedHashMap 在 HashMap 的基础上简单的增加了链表结构,就形成了节点的顺序,非常巧妙。

    ------------------------------------- END -------------------------------------

    相关文章

      网友评论

          本文标题:09-LinkedHashMap 核心源码分析(集合)

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