LinkedHashMap源码解析

简介: 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());
        }
    }

实现

![在这里插入图片描述](https://ucc.alicdn.com/images/user-upload-01/20190413100631395.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly94aW5kb28uYmxvZy5jc2RuLm5ldA==,size_16,color_FFFFFF,t_70 =400x)

  从类图其实可以看出来,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;
    }
}


目录
相关文章
|
人工智能 机器人 人机交互
哥大华人开发人脸机器人,照镜子自主模仿人类表情超逼真
【4月更文挑战第3天】哥伦比亚大学研究人员开发了一款名为Emo的机器人,能观察并模仿人类面部表情,实现更自然的人机交互。Emo配备26个面部执行器和高分辨率摄像头,通过“自我建模”学习模仿表情,并能预测人类表情变化。这一创新有望改善人机理解和响应情绪的能力,应用于教育、医疗等领域,但也引发了关于情感依赖和伦理问题的讨论。
432 4
哥大华人开发人脸机器人,照镜子自主模仿人类表情超逼真
|
数据库
ACN规则深度解密-全网最细的剖析
ACN规则深度解密-全网最细的剖析
LabVIEW使用VI脚本创建和打开VI
LabVIEW使用VI脚本创建和打开VI
601 2
|
机器学习/深度学习 人工智能 自然语言处理
探索深度学习在游戏开发中的创新应用
【8月更文挑战第11天】深度学习技术在游戏开发中的应用为游戏产业带来了前所未有的变革和机遇。通过不断探索和创新应用,我们有理由相信未来的游戏将会更加智能、丰富和引人入胜。
|
安全 API 调度
HarmonyOS学习路之开发篇—流转
随着全场景多设备生活方式的不断深入,用户拥有的设备越来越多,每个设备都能在适合的场景下提供良好的体验,例如:手表可以提供及时的信息查看能力,电视可以带来沉浸的观影体验。但是,每个设备也有使用场景的局限,例如:在电视上输入文本相对手机来说是非常糟糕的体验。当多个设备通过分布式操作系统能够相互感知、进而整合成一个超级终端时,设备与设备之间就可以取长补短、相互帮助,为用户提供更加自然流畅的分布式体验。
|
机器学习/深度学习 传感器 人工智能
构建未来:AI驱动的自适应交通管理系统
【5月更文挑战第21天】 在本文中,我们将探讨一个由人工智能(AI)技术驱动的自适应交通管理系统的架构和实现。该系统利用机器学习算法实时分析交通数据,预测并优化交通流,从而减少拥堵,提高道路使用效率。通过与传统交通管理方法的比较,我们展示了AI技术如何提升城市交通管理的智能化水平,以及这些技术对环境、经济和社会的潜在积极影响。
388 3
|
索引 Python
【Python 基础】解释Range函数
【5月更文挑战第6天】【Python 基础】解释Range函数
|
数据可视化 jenkins vr&ar
python3用ARIMA模型进行时间序列预测
python3用ARIMA模型进行时间序列预测
|
安全 数据处理 Android开发
安卓隐私权政策和Google Play规范更新
【4月更文挑战第14天】谷歌针对安卓平台的隐私权政策和Google Play规范进行重要更新,强化用户隐私保护和安全标准。新政策强调最小化数据收集,要求开发者明确告知用户敏感数据用途,并限制不必要的后台数据处理。Google Play规范更新要求应用详述数据收集方式,增加安全审查机制,确保无恶意代码。开发者面临调整,但有机会提升应用安全标准,赢得用户信任。用户数据安全得到提升,移动生态系统将更健康、可持续。
447 1
|
存储 分布式计算 大数据
现代化数据库技术——面向大数据的分布式存储系统
传统的关系型数据库在面对大规模数据处理时遇到了诸多挑战,而面向大数据的分布式存储系统应运而生。本文将深入探讨现代化数据库技术中的分布式存储系统,包括其优势、工作原理以及在大数据领域的应用。

热门文章

最新文章