LinkedHashMap源码解析

相信即便是Java初学者都应该用过Java中的HashMap和TreeMap,但貌似大多数人都没怎么用过LinkedHashMap,对其知之甚少。因为基本上大多数情况下TreeMap和HashMap都能满足需求,只有在需要map中K-V保持一定顺序时才会用到LinkedHashMap。所以保序是LinkedHashMap较HashMap和TreeMap最大的特点,至于保什么序后面会详细讲解。
在这里插入图片描述
  从类图其实可以看出来,LinkedHashMap其实是完全继承于HashMap的,甚至好多地方干脆复用了HashMap的源码。其实可以认为HashMap的功能是LinkedHashMap的子集,HashMap可以做的LinkedHashMap都可以做。

如何使用

其使用方式和HashMap一致,但默认是能保持插入顺序的,所以使用Iterator比例keySet或者entrySet时可以得到和插入顺序一致的结果。

    public static void main(String[] args) {
        Map<Integer, Integer> map = new LinkedHashMap<>();
        map.put(4,2);
        map.put(2,4);
        map.put(3,5);
        Iterator<Integer> iterator = map.keySet().iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }

它不仅仅能保持插入顺序,也可以看元素是否访问调整顺序,下面代码和上面代码的区别是多了构造函数和一个元素的get。但迭代的结果完全不同。LinkedHashMap对访问调序的支持为简单实现LRUCache奠定了基础。

    public static void main(String[] args) {
        Map<Integer, Integer> map = new LinkedHashMap<>(8, (float)0.75, true);
        map.put(4,2);
        map.put(2,4);
        map.put(3,5);
        map.get(4);
        Iterator<Integer> iterator = map.keySet().iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }
    }

实现

在这里插入图片描述
  从类图其实可以看出来,LinkedHashMap其实是完全继承于HashMap的,甚至好多地方干脆复用了HashMap的源码。其实可以认为HashMap的功能是LinkedHashMap的子集,HashMap可以做的LinkedHashMap都可以做。
  其实说白了,LinkedHashMap其实就是在HashMap+链表,就是用双链表把HashMap中的每个Node串起来,可以看如下示意图,黄色线条代表链表中的关系,主体结构还是HashMap中的结构,关于HashMap可以看我另一篇博客Java HashMap源码浅析 。所以较HashMap的源码,LinkedHashMap就是多加了一些双链表的操作(插入、删除、节点挪动到尾部……)。
在这里插入图片描述

源码

初始化

在这里插入图片描述
  LinkedHashMap的构造函数和HashMap的差不多类似,但多出来上图中的最后一个,其中参数多了一个boolean 类型的accessOrder,这个其实是否在节点被访问和变更后将其移动到双向链表的末尾,这也是文章最后实现LRUCache的关键参数。

    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

Entry

Entry继承自HashMap.Node<K,V>,就是在HashMap.Node<K,V>的基础上只添加了双向链表的前后指针,代码很简单如下。

    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);
        }
    }

put & get & resize & removeNode

其实LinkedHashMap中没有自己的put & get & resize & removeNode方法,完全是继承了HashMap中的方法。那肯定你也会好奇,LinkedHashMap中肯定每次增删改查总是会涉及到对双链表的操作,这是如何实现的?这个时候我们需要回到HashMap的源码中去。

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

如果你之前看HashMap的代码,你可能注意到了这三个没有实现的方法,你可能会很好奇他们有什么用。这三个方法在HashMap的各种操作中被用到了,看名字和注释也能看出来是在节点操作后做一些工作。可惜在HashMap中没用,你不看LinkedHashMap的源码肯定会感到莫名其妙的。

    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)
            head = a;
        else
            b.after = a;
        if (a == null)
            tail = b;
        else
            a.before = b;
    }

    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);
        }
    }

    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;
        }
    }

afterNodeRemoval()就是在Map中元素被移除后也移除双链表中相应的元素。 afterNodeInsertion()就是额外在双链表尾部插入新元素。但afterNodeAccess()就比较奇怪了,它是把某个元素挪动到队列的尾部,这有啥用?
  afterNodeAccess分别在putVal、merge、replace……总之所有有变动的地方调用,这以为着map中最新变动的值肯定是会在链表尾部,相反最旧的就在头部了(需要在构造函数中开启accessOrder)。
  在afterNodeInsertion()中我们还看到了removeEldestEntry(first),就是在插入新元素后移除最老的元素。 LinkedHashMap中默认是false,也就是不移除。如果我们继承了LinkedHashMap并对其重载,然后结合afterNodeAccess,就可以对最近最久未访问的元素做清理,不就是有个LRUCache了吗。

    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }
```  

### LinkedHashMap如何实现遍历时的保序 
 
  开头说过,LinkedHashMap和HashMap最大的一个区别就是前者能实现遍历的保序。可以按插入顺序或者最久访问顺序遍历,如何实现的?其实看下keySet() values() entrySet()这几个key value k-v遍历方法就知道了。HashMap中无法保存顺序信息,但双链表可以啊,所以为了获取顺序信息,它们不是HashMap中从map中获取数据,而是从双向链表中获取。  
```java
    public Set<K> keySet() {
        Set<K> ks = keySet;
        if (ks == null) {
            ks = new LinkedKeySet();
            keySet = ks;
        }
        return ks;
    }

    final class LinkedKeySet extends AbstractSet<K> {
        public final int size()                 { return size; }
        public final void clear()               { LinkedHashMap.this.clear(); }
        public final Iterator<K> iterator() {
            return new LinkedKeyIterator();
        }
        public final boolean contains(Object o) { return containsKey(o); }
        public final boolean remove(Object key) {
            return removeNode(hash(key), key, null, false, true) != null;
        }
        public final Spliterator<K> spliterator()  {
            return Spliterators.spliterator(this, Spliterator.SIZED |
                                            Spliterator.ORDERED |
                                            Spliterator.DISTINCT);
        }
        public final void forEach(Consumer<? super K> action) {
            if (action == null)
                throw new NullPointerException();
            int mc = modCount;
            for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
                action.accept(e.key);
            if (modCount != mc)
                throw new ConcurrentModificationException();
        }
    }

values() entrySet()方法实现类似,就不全贴了。

LRUCache

上文提到多次LRUCache,其中LRU是Least Recently Used的缩写,其实就是想在有限的存储空间里保留更多有价值数据的一种方式。其实现的依旧就是最佳被使用的数据将来还被使用的概率更高,这个现象在计算机领域非常明显,很多优化就是基于此的。LRUCache就是这样一种存储的实现,它的难点就在于如何高效地剔除掉最旧的数据,以及如何维护数据的新旧度。
  有了LinkedHashMap后,我们就可以很简单的实现一个LRUCache。依赖Linked和HashMap的结合,查询时可以从HashMap中以O(1)的时间复杂度查询,数据过期也可以用O(1)的时间复杂度从Linked中删除。LRUCache就是HashMap和Linked二者完美结合的体现。
  一个LRUCache的完整代码如下,没错 是完整的代码,就是这么简单,主要的逻辑LinkedHashMap里都已经帮你实现了,你只需要稍微封装下就可以了。其实只需要重载下HashMap中的removeEldestEntry()方法就行,这个方法会在新节点插入或者旧节点访问后被调用。

import java.util.LinkedHashMap;
import java.util.Map;

public class LRUCache<K,V> extends LinkedHashMap<K,V> {
    private int maxCap;
    @Override
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return size() > this.maxCap;
    }
    public LRUCache(int capacity) {
        super(capacity, (float)0.75, true);
        this.maxCap = capacity;
    }
}
xindoo CSDN认证博客专家 Linux 分布式 Spring
一个有趣有料的程序猿,9年技术博主,曾在阿里做过3年运维相关工作,现为某厂Java后端开发工程师,拥有丰富的挖坑踩坑填坑背锅经验[狗头],专注于Java,对操作系统、网络、编译原理也有涉猎,目前正在写一门简易的脚本语言。

专为程序员设计的数学课

11-11
<p> 限时福利限时福利,<span>15000+程序员的选择!</span> </p> <p> 购课后添加学习助手(微信号:csdn590),按提示消息领取编程大礼包!并获取讲师答疑服务! </p> <p> <br> </p> <p> 套餐中一共包含5门程序员必学的数学课程(共47讲) </p> <p> 课程1:《零基础入门微积分》 </p> <p> 课程2:《数理统计与概率论》 </p> <p> 课程3:《代码学习线性代数》 </p> <p> 课程4:《数据处理的最优化》 </p> <p> 课程5:《马尔可夫随机过程》 </p> <p> <br> </p> <p> 哪些人适合学习这门课程? </p> <p> 1)大学生,平时只学习了数学理论,并未接触如何应用数学解决编程问题; </p> <p> 2)对算法、数据结构掌握程度薄弱的人,数学可以让你更好的理解算法、数据结构原理及应用; </p> <p> 3)看不懂大牛代码设计思想的人,因为所有的程序设计底层逻辑都是数学; </p> <p> 4)想学习新技术,如:人工智能、机器学习、深度学习等,这门课程是你的必修课程; </p> <p> 5)想修炼更好的编程内功,在遇到问题时可以灵活的应用数学思维解决问题。 </p> <p> <br> </p> <p> 在这门「专为程序员设计的数学课」系列课中,我们保证你能收获到这些:<br> <br> <span> </span> </p> <p class="ql-long-24357476"> <span class="ql-author-24357476">①价值300元编程课程大礼包</span> </p> <p class="ql-long-24357476"> <span class="ql-author-24357476">②应用数学优化代码的实操方法</span> </p> <p class="ql-long-24357476"> <span class="ql-author-24357476">③数学理论在编程实战中的应用</span> </p> <p class="ql-long-24357476"> <span class="ql-author-24357476">④程序员必学的5大数学知识</span> </p> <p class="ql-long-24357476"> <span class="ql-author-24357476">⑤人工智能领域必修数学课</span> </p> <p> <br> 备注:此课程只讲程序员所需要的数学,即使你数学基础薄弱,也能听懂,只需要初中的数学知识就足矣。<br> <br> 如何听课? </p> <p> 1、登录CSDN学院 APP 在我的课程中进行学习; </p> <p> 2、登录CSDN学院官网。 </p> <p> <br> </p> <p> 购课后如何领取免费赠送的编程大礼包和加入答疑群? </p> <p> 购课后,添加助教微信:<span> csdn590</span>,按提示领取编程大礼包,或观看付费视频的第一节内容扫码进群答疑交流! </p> <p> <img src="https://img-bss.csdn.net/201912251155398753.jpg" alt=""> </p>
©️2020 CSDN 皮肤主题: 猿与汪的秘密 设计师: 上身试试 返回首页
实付0元
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值