LinkedHashMap简单学习(基于JDK 1.8)

1. 前言

1.1 絮絮叨叨

  • 第一次真正意义上地接触LinkedHashMap,来自工作中的一个需求:
    • SQL查询计划的生成,依赖于Hive的元数据
    • 原始的做法是,通过try-with-resource语句,每次访问元数据都新建一个client并立即回收
    • 这样的client连接属于短连接,如果每次建立、释放连接的时间占比很高,短连接的做法并不可行
    • 因此,想使用长连接,例如连接池,来改造访问逻辑,提高元数据的访问效率
  • 既然使用连接池,那就需要及时关闭长时间空间的连接,避免占用资源
  • 因此,如何实现空闲连接自动释放成为问题
  • 作为菜鸟:我希望在连接中增加一个accessTime字段,然后遍历池中的连接,如果发现距上一次使用超过一定时间,就可以释放连接
  • 这其实就跟LRU缓存很像,最近最少使用的连接排位逐渐靠前,达到阈值就被释放
  • 同事推荐使用LinkedHashMap实现LRU缓存,及时清理空闲连接
  • 通过简单的学习,自己了解到:LinkedHashMap实现LRU缓存,最重要的是重写removeEldestEntry()方法,实现清理空闲连接的逻辑

1.2 按照某种顺序连接哈希表中的所有entry

  • 单从类名上看,LinkedHashMap应该是具有链表特性的哈希表
  • 回想一下链表的特性:
    • 相对数组,链表不需要连续的存储空间,节点(元素)之间通过next引用连接
    • 链表中的元素,只能顺序访问,不支持随机访问(顺着next引用查找元素)
    • 支持快速地插入或删除元素:一旦确认位置,只需要修改节点之间的引用即可,不存在元素的移位
  • 那具有链表特性的哈希表,链接的不可能是桶,更可能的是entry
  • 问题1: 是桶中的entry使用链表连接,还是整个哈希表中的entry使用链表连接?
  • 分析一下
    • HashMap中,同一个桶中的entry已经使用链表连接了
    • 这里的链表更有可能是针对哈希表中的所有entry
  • 问题2: 将所有entry连接起来,有啥用?
    • 学习HashMap和TreeMap时,提到过一个关于entry是否有序的区别
    • TreeMap实现了SortedMap,支持按key的自然顺序或自定义比较器的顺序访问entry
    • HashMap既不保证entry的顺序,还可能因为扩容导致entry的顺序在一段时间内发生变化
    • 如果按照entry的插入顺序,将哈希表中的所有entry连接起来,那就可以实现支持插入顺序的哈希表
    • 其实,LinkedHashMap不仅支持按照插入顺序组织entry,还支持按照访问顺序(LRU)组织entry:头部是最近最少访问的entry
    • 这也是为啥,同事推荐我使用LinkedHashMap实现LRU

1.3 LinkedHashMap的特性

  • 学习一个类之前,如果有完善的类注释,阅读类注释肯定是一个不错的选择

根据类注释,LinkedHashMap的特性如下

  • LinkedHashMap使用双向链表,维护了元素的顺序
    • LinkedHashMap不仅支持entry的插入顺序,还支持entry的访问顺序
    • 默认为entry的插入顺序,通过LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder)构造函数,可以创支持于访问顺序的LinkedHashMap
    • 整个双向链表分为head和tail,head指向最近最少访问的entry,tail指向最近刚访问的entry。
    • 一旦插入或访问entry,会引起entry的顺序变化(被移动得到末尾)
    • LinkedHashMap还提供removeEldestEntry()方法,通过重写该方法,可以在新增entry时,删除老旧的entry(位于头部)
    • 注意:
      • 如果基于插入顺序,put时更新key已映射的value,不会引起顺序变化;
      • 如果基于访问顺序,任何访问操作(如put、get)都将导致顺序的变化。例外: 通过iterator或for-each遍历entry,不影响entry的顺序
  • LinkedHashMap的优势:不仅解决了HashMap或HashTable中entry无序的情况,还相对TreeMap的有序实现更节约成本
    • 本人猜测是实现成本,LinkedHashMap是基于HashMap实现的,真正需要编写的代码较少
  • LinkedHashMap是非同步的,即非线程安全的
    • LinkedHashMap没有对应的线程安全的替代类,只能通过Collections.synchronizedMap()将其转为线程安全的map
  • LinkHashMap继承了HashMap,同样也允许null值:只允许一个key为null,允许多个value为null
  • 关于性能
    • 与HashMap一样,在hash散列均匀的情况下,LinkedHashMap可以提供常数级的访问效率
    • 由于需要维护entry中的双向链表,LinkedHashMap的性能稍逊于HashMap
    • 同时,对LinkedHashMap的遍历直接基于双向链表,而非基于桶(遍历特指iterator或for-each遍历,并非确定entry位置时的遍历)
    • 因此,LinkHashMap的遍历时间,与entry的数目成正比
  • LinkedHashMap使用fail-fast迭代器
    • 如果使用插入顺序,查询和更新操作不属于结构改变,新增、删除、rehash属于结构改变
    • 如果使用访问顺序,任何寻址(查、新增、更新和rehash)或删除操作都属于结构改变
    • 迭代器一旦创建,除了迭代器自身的remove方法,任何引起map结构改变的操作,都将使迭代器抛出ConcurrentModificationException

总结一下

  • LinkedHashMap使用双向链表维护了entry的顺序:插入顺序或访问顺序
  • LinkedHashMap允许null
  • LinkedHashMap是非线程安全的
  • LinkedHashMap使用fail-fast迭代器
  • 由于需要维护双向链表,LinkedHashMap的性能稍逊于其父类HashMap
  • LinkedHashMap的优势:entry有序,且实现成本较低

2. LinkedHashMap概述

2.1 类图

  • LinkedHashMap类的声明如下

    public class LinkedHashMap<K,V> extends HashMap<K,V>
        implements Map<K,V>
    
  • 类图,如下图所示
    在这里插入图片描述

从类图来看

  • LinkedHashMap继承了HashMap,说明它具有HashMap的特性(桶数组+ 链表 + 红黑树)
  • LinkedHashMap实现了Map接口,额,暂且认为和HashMap一样吧,这个作者的一个小失误,且一直没被源代码的维护人员校正 😂

2.2 成员变量及数据结构

  • LinkedHashMap继承了HashMap,那HashMap所特有的桶数组+ 链表 + 红黑树的结构LinkedHashMap也一样具有

  • 那是如何在所有的entry中维护一个双向链表的呢?从成员变量就可以解释这个问题

  • 新增了三个成员变量

    // 双向链表的头部:插入顺序时,是最先插入的entry;访问顺序时,是最近最少访问的entry
    transient LinkedHashMap.Entry<K,V> head;
    
    // 双向链表的尾部:插入顺序时,是最后插入的entry;访问顺序时,是最近刚访问的entry
    transient LinkedHashMap.Entry<K,V> tail;
    
    // 是否使用访问顺序,true表示使用
    final boolean accessOrder;
    
  • 其中,Entry的定义如下:增加了beforeafter引用,是实现双向链表的关键

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

  • 回忆之前学习HashMap时,关于数据结构的讲解
    • HashMap中,链表对应的是Node、红黑树对应的是TreeNode
    • TreeNode继承了LinkedHashMap.Entry,LinkedHashMap.Entry又继承了HashMap.Node
    • 到现在,作者都还有疑问:TreeNode为啥不直接继承HashMap.Node?因为TreeNode的实际使用中,好像没有用到 LinkedHashMap.Entry 中的新增属性
  • 在后面的学习中,通过newNode()newTreeNode()afterNodeAccess()afterNodeRemoval()方法,自己体会到了这样设计的原因
    • 在HashMap中,节点无非是链表节点 Node 或红黑树节点TreeNode
    • 为了实现双向链表,LinkedHashMap中的链表节点 Entry 相对父类 Node 增加了beforeafter引用
    • 接着,红黑树节点TreeNode继承 LinkedHashMap.Entry,这样LinkHashMap中的节点(链表节点或红黑树节点)具有beforeafter引用,使得双向链表连接所有entry成为可能
    • 除此之外,TreeNode可以向上转型为 LinkedHashMap.Entry,这样所有节点都当做LinkedHashMap.Entry进行处理,而无需关注是链表节点还是红黑树节点

总结

  • LinkedHashMap依靠Entry的beforeafter引用构建双向链表

  • 同时,LinkedHashMap类中的head和tail指出了双向链表的头尾,有助于双向链表的构建及顺序的维护(尾部插入、最近刚访问位于尾部等)

  • 如果,一个HashMap的示意图如下
    在这里插入图片描述

  • 使用LinkedHashMap后,示意图如下
    在这里插入图片描述

2.3 构造函数

  • LinkedHashMap提供如下构造函数

    // 创建一个指定了初始化容量和loadFactor的、基于插入顺序的LinkedHashMap
    // 对应HashMap的public HashMap(int initialCapacity, float loadFactor)
    public LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }
    
    // 创建一个指定了初始化容量的、基于插入顺序的LinkedHashMap
    // 对应HashMap的public HashMap(int initialCapacity) 
    public LinkedHashMap(int initialCapacity) {
        super(initialCapacity);
        accessOrder = false;
    }
    
    // 创建一个基于插入顺序的LinkedHashMap,容量和loadFactor使用默认值
    // 对应HashMap的public HashMap()
    public LinkedHashMap() {
        super();
        accessOrder = false;
    }
    
    // 基于已有的map,创建一个基于插入顺序的LinkedHashMap,容量和loadFactor使用默认值
    public LinkedHashMap(Map<? extends K, ? extends V> m) {
        super();
        accessOrder = false;
        putMapEntries(m, false);
    }
    
    // 指定初始化容量、loadFactor和顺序的LinkedHashMap,accessOrder为true表示访问顺序
    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }
    
  • 最后一个构造函数,是与父类相比,多出来的一个构函数,专为创建基于访问顺序的LinkedHashMap而准备

  • 实现LRU缓存时,都需要使用该构造函数

    new LinkedHashMap(capacity, loadFactor, true)
    

3. 查找方法

  • LinkedHashMap中,重写的方法很少,查找方法几乎都进行了重写
  • 目的: 为了支持访问顺序,一旦通过查找方法访问了entry,entry的顺序应该发生变化

3.1 get方法

  • get()方法的代码如下

    public V get(Object key) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
    }
    
  • 与HashMap中的方法相比,在获取到entry后,还需要判断是否为访问顺序;如果使用访问顺序,需要通过afterNodeAccess()方法调整该entry的位置

3.1.1 afterNodeAccess()方法

  • afterNodeAccess()方法在HashMap的put()方法中遇到过,但是当时说它是个空方法, LinkedHashMap重写了该方法

  • 按照本人的理解,afterNodeAccess 方法的作用:在LinkedHashMap使用访问顺序时,将刚访问过的entry移到双向链表末尾

    • 如果entry本身就在末尾,则不用移动

    • 如果entry处于双向链表的头部,则只需要断开与后继节点的关联,然后将其移到末尾
      在这里插入图片描述

    • 如果entry处于双向链表的中部,则需要先将前驱节点与后继节点连上,然后将其移到末尾(最后一部,需要 e.after = null
      在这里插入图片描述

  • 代码如下:不过自己觉得代码逻辑有点乱,貌似还有多余的部分 😂

    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) // 前驱节点为空,说明p在头部,直接将head指向后继节点
                head = a;
            else // 否则,前驱节点指向后继节点
                b.after = a;
            if (a != null)  // 后继节点指向前驱节点
                a.before = b;
            else // 这里的代码多余?如果后继节点为null,那p就是末尾了,根本进入不了if???
                last = b;
            if (last == null)  // 这里也是因为上一步导致的多余判断
                head = p;
            else { // 将p移到末尾
                p.before = last;
                last.after = p;
            }
            tail = p; // 更新tail引用
            ++modCount;
        }
    }
    

3.2 getOrDefault / containsValue方法

3.2.1 getOrDefault方法

  • 与get方法一样,getOrDefault()方法,也增加了对accessOrder为true的处理

    public V getOrDefault(Object key, V defaultValue) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)
            return defaultValue;
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
    }
    

3.2.2 containsValue方法

  • 查找类的方法,get、getOrDefault都重写了,按理说containsKey和containsValue也应该重写的

  • 但LinkedHashMap只重写了containsValue方法

  • 仔细想想也是有道理的:

    • 判断是否包含key,只需要通过getNode获取到key对应entry就行了
    • 判断是否包含value,需要遍历桶中的每个entry(双层循环,外层为桶,内层为桶中的元素)
    • LinkedHashMap中,所有的entry使用双向链表关联,查找value直接基于双向链表顺序查找即可
  • containsValue的代码如下,十分简单

    public boolean containsValue(Object value) {
        for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after) {
            V v = e.value;
            if (v == value || (value != null && value.equals(v)))
                return true;
        }
        return false;
    }
    

4. put方法

  • LinkedHashMap就没有重写put方法,因为HashMap中,put方法的核心方法putVal()已经未雨绸缪了

  • putVal()方法中,存在对entry被访问或新增entry后,调整双向链表的空方法:afterNodeAccess()afterNodeInsertion()

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        // 省略一些代码
        else {
            // 存在key的映射,则需要更新value;如果是访问顺序,需要将entry移到末尾
            if (e != null) { 
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e); // 将entry移到末尾
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict); // 新增节点,需要调整链表
        return null;
    }
    
  • 想要实现LinkedHashMap,只需要重写上述两个方法就可以了。

  • 所以,这也是为啥LinkedHashMap没有重写put() 方法

4.1 afterNodeInsertion方法

  • 一开始,自己认为 afterNodeInsertion() 方法要完成如下事情

    • 不管是插入顺序还是访问顺序,新增的entry都应该位于双向链表的尾部,由 afterNodeInsertion() 方法完成这一操作
    • 然后根据removeEldestEntry()的结果,来决定是否删除最老的entry
  • 后来一看,怎么 afterNodeInsertion() 方法的定义如下:入参都不是刚插入的entry

    void afterNodeInsertion(boolean evict) { // possibly remove eldest
        LinkedHashMap.Entry<K,V> first;
        // 头节点不为null,且需要删除最老的节点,则删除头结点
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            K key = first.key;
            removeNode(hash(key), key, null, false, true);
        }
    }
    
  • 其入参evict,是驱逐的意思,在put() --> putVal()时固定为true

  • 也就是说,在调用afterNodeInsertion() 方法时,evict固定为true

  • 是否会删除最老的entry,由removeEldestEntry()方法决定

  • removeEldestEntry()方法如下,总是返回false。

    • 即:LinkedHashMap就算使用访问顺序,也只是让最老的entry位于头部,并不会删除
    • 这也是为什么,在使用LinkedHashMap实现LRU时,一般都需要重写removeEldestEntry()方法,让其在某种情况下返回true,实现过期元素的清理
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }
    

4.2 newNode和newTreeNode方法

  • 问题来了:既然afterNodeInsertion() 方法只负责删除最老的元素,在哪里完成entry加入双向链表的呢?
  • 仔细阅读putVal()方法,发现新增entry时,通过newNode()newTreeNode()完成节点的新建
  • LinkedHashMap重写了这两个新建节点的方法,在这两个方法中完成了entry加入双向链表的逻辑

newNode()方法

  • 不再是简单的return new Node<>(),而是新建LinkedHashMap.Entry,并将其加入双向链表末尾
    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);
        // 将p放到链表末尾
        linkNodeLast(p);
        return p;
    }
    
    private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
        LinkedHashMap.Entry<K,V> last = tail;
        tail = p;
        // 原尾节点为null,表明p是第一个节点,head指向p
        if (last == null)
            head = p;
        else { // 否则,将p和原尾结点关联
            p.before = last;
            last.after = p;
        }
    }
    

newTreeNode方法

  • 不再是简单的return new TreeNode<>(),而是创建一个TreeNode,并将其加入双向链表尾部

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

5. remove方法 —— afterNodeRemoval

  • 在学习HashMap的remove方法时,两种remove方法的核心都是通过removeNode()方法实现节点的删除

  • removeNode()方法的末尾,有一个空的afterNodeRemoval()方法

  • 学了 afterNodeAccess 和afterNodeInsertion, 应该能触类旁通:LinkedHashMap会重写 afterNodeRemoval()方法,实现删除entry后的双向链表调整

  • afterNodeRemoval()方法,代码如下

    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; // 主动断开p与其他entry的关联
        if (b == null) // p是头节点,则head指向其后继节点
            head = a;
        else // 否则,前驱节点指向后继节点
            b.after = a;
        if (a == null)  // p是尾结点,tail指向前驱节点
            tail = b;
        else // 后继节点指向前驱节点
            a.before = b;
    }
    
  • 要是我写的话,我可能会这样实现

    void afterNodeRemoval(Node<K,V> e) { // unlink
         LinkedHashMap.Entry<K,V> p =
             (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
         // 先断开p与其他节点的关联
         p.before = p.after = null;
         // 情况1: p是头结点也是尾节点
         if (head == p && head == tail) {
         	head = tail = null;
         } else if (head == p) { // 情况2:p是头结点
         	a.before = null;
         	head = a;
         } else if (tail == p) { // 情况3:p是尾节点
         	b.after = null;
         	tail = b;
         } else { // 情况4:p是中间节点
         	b.after = a;
         	a.before = b;
         }
     }
    
  • 按照我的思路,发现写源码的人,脑袋就是灵活

    • b为空,说明p是头结点,直接将head指向a;可以涵盖情况1(head = a = null)、情况2(head = a, a != null
    • 否则,将b.after指向a;可以涵盖情况3(b.after = a = null)、情况4(b.after = a, a != null
    • a为空,说明p是尾结点,直接将tail指向b;可以涵盖情况1(tail = b = null)、情况3(tail = b, b! = null
    • 否则,将a.before指向b;可以涵盖情况2(a.before = b = null)、情况4(a.before = b, b! = null
  • 这里就不画图展示了,读者可以先画出四种情况的图,再结合图阅读源代码

6. 总结

6.1 基于LinkedHashMap实现LRU缓存

  • 在不考虑线程安全的情况下,基于LinkedHashMap实现LRU缓存,是最简单快捷的方式

  • 只需要重写removeEldestEntry()方法,使其在达到阈值时返回true,即可删除最近最少使用的数据

    public class LRUCache<K, V> extends LinkedHashMap<K, V> {
        private int maxSize;
    
        public LRUCache(int maxSize) {
            super(16, 0.75f, true);
            this.maxSize = maxSize;
        }
    
        @Override
        protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
            return size() > maxSize;
        }
    
        public static void main(String[] args) {
            LRUCache<String, Integer> cache = new LRUCache<>(5);
            cache.put("张三", 21);
            cache.put("lucy", 24);
            cache.put("john", 30);
            cache.put("jack", 25);
            cache.put("李四", 20);
            // 插入第6个元素,最后发现最先插入的key-value被删除
            cache.put("王二", 32);
            System.out.println("张三的信息已不存在: " + !cache.containsKey("张三"));
        }
    }
    

6.2 LinkedHashMap如何维护顺序的?

  • 首先,LinkedHashMap基于HashMap实现了有序的哈希表

  • 其次,LinkedHashMap的有序是通过双向链表维护的,其在数据结构上就做了工作

    • 节点类型为LinkedHashMap.Entry,相对父类HashMap.Node,多了beforeafter引用,用于在entry间构建双向链表
    • 为了更好地维护entry的顺序、更快地查找entry,LinkedHashMap类中,增加了headtail引用
    • LinkedHashMap类中,还增加了accessOrder变量,以决定是维护插入顺序还是访问顺序
  • 然后,LinkedHashMap还通过重写以下方法,维护entry在双向链表中的顺序

    void afterNodeAccess(Node<K,V> p)
    void afterNodeRemoval(Node<K,V> p)
    Node<K,V> newNode(int hash, K key, V value, Node<K,V> next)
    TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next)
    
  • 除此之外,提供了删除最近最少访问entry的方法,有助于实现LRU

    void afterNodeInsertion(boolean evict) // 关键需要重写removeEldestEntry()
    

6.3 LinkedHashMap与HashMap的联系

二者的联系:LinkedHashMap继承了HashMap,基于双向链表维护了entry的顺序

  • 数据结构上:Entry增加beforeafter引用,LinkedHashMap增加headtail引用
  • 继承与重载上:LinkedHashMap相当于站在巨人的肩膀上做事,只实现了一些关键的方法
    • get() 和getOrDefault() ,增加了维护访问顺序的代码
    • containsValue(),不再基于桶遍历entry,而是直接基于双向链表遍历entry
    • putVal()中,新增节点的newNode() 和newTreeNode() 方法都重写了,实现了节点上链
    • 同时,putVal()中,afterNodeInsertion()被重写,可以在removeEldestEntry() 返回true时,实现LRU缓存
    • removeNode()方法中,最后调用的afterNodeRemoval() 方法以删除双向链表中的对应节点

6.4 LinkedHashMap与HashMap的异同

相同点:

  1. 都实现了Map接口,允许null
  2. 都是非线程安全的map类,需要通过Collections.synchronizedMap()转为安全的map类,或使用已有的、线程安全的替代类
  3. 都使用fail-fast迭代器,一旦创建好迭代器,除非使用迭代器自身的remove方法,其他任何改变map结构的方法,都将触发ConcurrentModificationException
  4. 其他的,扩容、链表转红黑树、红黑树退回链表等,LinkHashMap都和HashMap一样

不同点

  1. 最大的不同点:LinkedHashMap通过双向链表为了entry的顺序,插入顺序或访问顺序;HashMap中的entry不仅无序,迭代结果还可能在一段时间内发生变化
  2. 其他的,无非是实现上的不同,例如,containsValue(),不再基于桶遍历entry,而是直接基于双向链表遍历entry

参考链接:

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
LinkedHashMap 是继承自 HashMap 的类,在 HashMap 的基础上通过维护一条双向链表来解决无法保持遍历顺序和插入顺序一致的问题,并提供了对访问顺序的支持。在 JDK 1.8 版本中,LinkedHashMap 使用了和 HashMap 相同的底层数据结构,即拉链式散列结构,并在解决长链表问题上引入了红黑树优化。这样,LinkedHashMap 可以提供高效的增删改查操作,并且在遍历时可以按照插入或访问的顺序进行遍历LinkedHashMap 的底层数据结构和 HashMap 一样,都是使用数组加链表或红黑树的方式来处理冲突。每个数组元素都是一个链表或红黑树的头节点,每个节点包含一个键值对。当插入或查找元素时,根据键的哈希值找到对应的数组下标,然后在链表或红黑树中进行操作。 在 LinkedHashMap 中,除了继承了 HashMap 的方法,还覆写了部分方法来维护双向链表。具体来说,LinkedHashMap 在 put、remove 和 get 等方法中添加了对双向链表的操作,以保证插入和访问的顺序。当插入一个新的元素时,LinkedHashMap 会将该元素插入到链表的末尾;当访问一个已有元素时,LinkedHashMap 会将该元素移动到链表的末尾。通过这种方式,LinkedHashMap 可以保持元素的插入或访问顺序,实现了有序遍历的效果。 总结起来,LinkedHashMap 的底层原理是在 HashMap 的基础上通过维护一条双向链表来实现插入和访问的顺序,而在 JDK 1.8 中,LinkedHashMap 使用了和 HashMap 相同的底层数据结构,即拉链式散列结构,并引入了红黑树优化。这样,LinkedHashMap 可以提供高效的增删改查操作,并且在遍历时可以按照插入或访问的顺序进行遍历。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [LinkedHashMap(JDK1.8)源码解析](https://blog.csdn.net/qq_41242680/article/details/114637171)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值