Java集合之LinkedHashMap

本文深入解析了LinkedHashMap的内部机制,包括其继承关系、存储结构及源码分析,重点介绍了如何利用LinkedHashMap实现LRU缓存策略。

1. 简介

      LinkedHashMap内部维护了一个双向链表,能保证元素按插入的顺序访问,也能以访问顺序访问所以可以用来实现LRU缓存策略

2. 继承关系

在这里插入图片描述
      通过继承关系,我们可以看到继承了 HashMap,所以其拥有 HashMap的所有特性,并且额外增加了按顺序访问的特性。

3. 存储结构

在这里插入图片描述
      添加删除元素的时候需要同时维护在HashMap中的存储,也要维护在LinkedList中的存储,所以性能上来说会比HashMap稍慢。

4. 深入源码

4.1 属性

    /**
     * 指向双向链表的头节点指针
     */
    transient LinkedHashMap.Entry<K,V> head;

    /**
     * 指向双向链表的尾节点指针
     */
    transient LinkedHashMap.Entry<K,V> tail;

    /**
     * true:双向链表按访问顺序排序
     * false:双向链表按插入顺序访问
     */
    final boolean accessOrder;

      (1)head

      双向链表的头节点,旧数据存在头节点

      (2)tail

      双向链表的尾节点,新数据存在尾节点

      (3)accessOrder

      是否需要按访问顺序排序,如果为false则按插入顺序存储元素,如果是true则按访问顺序存储元素。

4.2 内部类

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

	static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
   }

      可以看到 LinkedHashMap 不仅仅采用了 数组 + 链表 + 红黑树的结构,节点和节点之间还通过两个指针链接成了双向链表。

4.3 构造方法

	// 构造一个空的按插入顺序排序的 LinkedHashMap 实例,其默认初始容量(16)和负载因子(0.75)。
    public LinkedHashMap() {
        super();
        accessOrder = false;
    }

	// 构造一个空的按插入顺序排序的 LinkedHashMap 实例,初始容量为 initialCapacity 的二次幂取整的值 和负载因子(0.75)。
    public LinkedHashMap(int initialCapacity) {
        super(initialCapacity);
        accessOrder = false;
    }

	// 指定 initialCapacity 和 加载因子
    public LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }

    public LinkedHashMap(Map<? extends K, ? extends V> m) {
        super();
        accessOrder = false;
        putMapEntries(m, false);
    }
    
	// 该构造方法accessOrder从构造方法参数传入,如果传入true,则就实现了按访问顺序存储元素,这也是实现LRU缓存策略的关键。
    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

      前四个构造方法accessOrder都等于false,说明双向链表是按插入顺序存储元素。

      最后一个构造方法accessOrder从构造方法参数传入,如果传入true,则就实现了按访问顺序存储元素,这也是实现LRU缓存策略的关键。

在这里插入图片描述

4.4 添加及删除元素

      通过观察LinkedHashMap,可以看到其并没有putremove方法,说明其调用的是父类 HashMap的方法,那么其是怎么实现按插入顺序或访问顺序访问的特性的呢

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
       // 代码省略
           
        afterNodeAccess(e);
   		// 代码省略
        afterNodeInsertion(evict);
        return null;
    }
    final Node<K,V> removeNode(int hash, Object key, Object value,
                               boolean matchValue, boolean movable) {
        // 代码省略
        afterNodeRemoval(node);
        return null;
    }
	// 该方法位于 LinkedHashMap 中
    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;
    }

      我们直到在 afterNodeInsertionafterNodeAccessafterNodeRemoval 这三个方法中为空实现,而 LinkedHashMap中却实现了该方法,所以这便是LinkedHashMap 特性的决定方法。

4.4.1 afterNodeInsertion

      该方法在节点被插入之后调用该方法

    void afterNodeInsertion(boolean evict) { // possibly remove eldest
        LinkedHashMap.Entry<K,V> first;
        // 如果evict为true,且头节点不为空,且确定移除最老的元素,那么就调用HashMap.removeNode()把双向链表的头节点移除
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            K key = first.key;
            removeNode(hash(key), key, null, false, true);
        }
    }

(1)如果evicttrue,且头节点不为空,且确定移除最老的元素,那么就调用HashMap.removeNode()把头节点移除(这里的头节点是双向链表的头节点,而不是某个桶中的第一个元素);

(2)HashMap.removeNode()HashMap中把这个节点移除之后,会调用afterNodeRemoval()方法;

(3)afterNodeRemoval()方法在LinkedHashMap中也有实现,用来在移除元素后修改双向链表,见下文;

(4)默认removeEldestEntry()方法返回false,也就是不删除元素。

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

	// 这里调用的是 HashMap 中的方法删除,已在 HashMap 中分析过
    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;
                // 调用删除节点后的方法
                afterNodeRemoval(node);
                return node;
            }
        }
        return null;
    }

4.4.2 afterNodeRemoval

      该方法在节点被删除之后调用该方法。

	// e 为被删除的节点
	// 实现便是在双向链表中删除一个节点的实现
    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;
    }

      经典的把节点从双向链表中删除的方法。

4.4.3 afterNodeAccess

      该方法在节点被访问之后调用该方法。

      在节点访问之后被调用,主要在put()已经存在的元素或get()时被调用,如果accessOrdertrue,调用这个方法把访问到的节点移动到双向链表的末尾。

    void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMap.Entry<K,V> last;
        // 如果accessOrder为true,并且访问的节点不是尾节点
        if (accessOrder && (last = tail) != e) {
            LinkedHashMap.Entry<K,V> p =
                (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
             // 把p节点从双向链表中移除    
            p.after = null;
            if (b == null)
                head = a;
            else
                b.after = a;
            if (a != null)
                a.before = b;
            else
                last = b;
            // 把p节点放到双向链表的末尾    
            if (last == null)
                head = p;
            else {
                p.before = last;
                last.after = p;
            }
            tail = p;
            ++modCount;
        }
    }

(1)如果accessOrder为true,并且访问的节点不是尾节点;

(2)从双向链表中移除访问的节点;

(3)把访问的节点加到双向链表的末尾;(末尾为最新访问的元素)

示例一
public class Test {

    public static void main(String[] args) {
        LinkedHashMap linkedHashMap = new LinkedHashMap(16,0.75f,true);
        linkedHashMap.put("1","1");
        linkedHashMap.put("2","2");
        linkedHashMap.put("3","3");

        Set set = linkedHashMap.entrySet();
        System.out.println(set);

        linkedHashMap.get("1");
        set = linkedHashMap.entrySet();
        System.out.println(set);

        linkedHashMap.get("2");
        set = linkedHashMap.entrySet();
        System.out.println(set);
    }

}

      我们在构造方法中指定的是true,那么便说明我们采用的是按照访问顺序来访问对链表进行访问。

      输出如下:
在这里插入图片描述
      可以明显看到,我们访问的元素被添加到了双向链表的末尾。

示例二
public class Test {

    public static void main(String[] args) {
        LinkedHashMap linkedHashMap = new LinkedHashMap(16,0.75f,false);
        linkedHashMap.put("1","1");
        linkedHashMap.put("2","2");
        linkedHashMap.put("3","3");

        Set set = linkedHashMap.entrySet();
        System.out.println(set);

        linkedHashMap.get("1");
        set = linkedHashMap.entrySet();
        System.out.println(set);

        linkedHashMap.get("2");
        set = linkedHashMap.entrySet();
        System.out.println(set);
    }

}

      如果我们按照访问插入顺序来访问链表还是上面的输出吗?

      输出如下:
在这里插入图片描述
      可以明显看到输出两者不同,符合我们对该方法的分析。

4.5 获取元素

4.5.1 get(Object key)

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

    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

      如果查找到了元素,且accessOrdertrue,则调用afterNodeAccess()方法把访问的节点移到双向链表的末尾。

(1)LinkedHashMap继承自HashMap,具有HashMap的所有特性;

(2)LinkedHashMap内部维护了一个双向链表存储所有的元素;

(3)如果accessOrderfalse,则可以按插入元素的顺序遍历元素;

(4)如果accessOrder为true,则可以按访问元素的顺序遍历元素;

(5)LinkedHashMap的实现非常精妙,很多方法都是在HashMap中留的钩子(Hook),直接实现这些Hook就可以实现对应的功能了,并不需要再重写put()等方法;

(6)默认的LinkedHashMap并不会移除旧元素,如果需要移除旧元素,则需要重写removeEldestEntry()方法设定移除策略;

(7)LinkedHashMap可以用来实现LRU缓存淘汰策略;

4.6 LRU 实现

      LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

      所以我们可以这样做,刚被访问过的元素将其放到链表尾(链表头),刚被添加的元素也被放入到链表尾(链表头),而当缓存大小满了之后,则删除链表头(链表尾)的元素,这样便实现了LRU。

      在LinkedHashMap中,如果我们访问了一个元素,则一定会调用afterNodeAccess方法,而该方法将链表中的节点放入到了链表尾,而在添加元素时,同样调用了该方法,所以我们只需要在缓存满时删除表头的元素即可。回到删除元素的方法afterNodeInsertion,在该方法中调用了removeEldestEntry方法,默认返回false,所以我们只要重写该方法即可。在缓存满时删除表头节点。

/**
 * @author wangzhao
 * @date 2019/12/7 16:26
 */
public class LRUCache extends LinkedHashMap {

    // 缓存的最大容量
    private  int capacity;

    public LRUCache(int capacity){
        // 注意,这里一定要将 accessOrder 指定为 true,只有其为true,afterNodeAccess 方法才能生效
        super(capacity,0.75f,true);
        this.capacity = capacity;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > this.capacity;
    }

    public static void main(String[] args) {
        LRUCache lru = new LRUCache(3);
        lru.put("1","1");
        lru.put("2","2");
        lru.put("3","3");

        System.out.println(lru.entrySet());
        // 输出: [1=1, 2=2, 3=3]
        
        lru.get("1");
        System.out.println(lru.entrySet());
        // 输出: [2=2, 3=3, 1=1]

        lru.get("2");
        System.out.println(lru.entrySet());
		// 输出: [3=3, 1=1, 2=2]

        lru.get("3");
        System.out.println(lru.entrySet());
		// 输出: [1=1, 2=2, 3=3]

        lru.put("4","4");
        System.out.println(lru.entrySet());
        // 输出:[2=2, 3=3, 4=4]
    }

}
<think>嗯,用户让我介绍一下Java集合中的LinkedHashMap,还要结合源码分析。首先,我得回忆一下LinkedHashMap的基本概念和它在Java集合框架中的位置。LinkedHashMap是HashMap的子类,所以它应该具有HashMap的所有特性,比如允许null键值,非同步等等。但它的特殊之处在于维护了一个双向链表,用来保持插入顺序或者访问顺序。这可能就是用户想深入了解的地方。 接下来,我需要考虑用户的需求。他们可能已经对HashMap有一定的了解,现在想进一步学习LinkedHashMap的工作原理和内部实现。所以,我应该先对比HashMap和LinkedHashMap的区别,强调LinkedHashMap的有序性。然后,可能需要讲解LinkedHashMap的数据结构,比如Entry节点的结构,双向链表是如何维护的。 然后,源码分析部分需要深入LinkedHashMap的关键方法,比如put、get、remove,以及如何维护访问顺序。比如,在HashMap的put方法中,LinkedHashMap可能重写了某些方法,比如afterNodeAccess和afterNodeInsertion,这些钩子方法的作用是什么。另外,访问顺序模式下,最近访问的节点会被移动到链表末尾,这在实现LRU缓存时很有用,这个点可能也需要提到。 用户可能还想知道LinkedHashMap如何实现迭代顺序的有序性。这时候需要分析迭代器的实现,可能是通过遍历双向链表而不是像HashMap那样遍历桶数组。此外,构造方法中的accessOrder参数的作用,以及如何影响元素顺序,这也是关键点。 另外,要注意到LinkedHashMap在实现LRU缓存时的应用。当accessOrder设置为true时,每次访问元素都会调整顺序,当达到容量时,会移除最老的元素。这部分可能需要结合removeEldestEntry方法的源码,说明如何扩展这个方法来控制缓存策略。 在结构上,应该先介绍LinkedHashMap的基本概念,然后数据结构,接着关键源码分析,最后应用场景。可能还需要给出一个简单的例子,比如如何用LinkedHashMap实现LRU缓存,这样用户更容易理解。 需要确认是否覆盖了主要的方法和内部类,比如Entry的结构,是否有新增的字段如before和after。同时,要注意源码的版本是否正确,假设是Java 8的源码,因为不同版本可能有差异。例如,在Java 8中,HashMap引入了树化结构,但LinkedHashMap仍然使用双向链表,这部分是否需要提及? 在讲解源码时,可能需要用代码块展示关键部分,比如Entry类的定义,以及afterNodeAccess方法的具体实现。但要注意不要粘贴过多代码,而是选择关键部分进行解释。 另外,用户可能对LinkedHashMap的性能感兴趣,比如与HashMap相比,插入、删除、访问的时间复杂度是否有变化。虽然LinkedHashMap维护了双向链表,但大多数操作的时间复杂度仍然是O(1),因为链表操作是常量时间的。 最后,总结LinkedHashMap的特点和适用场景,帮助用户理解何时应该选择LinkedHashMap而不是其他集合类。例如,需要保持插入顺序或访问顺序,或者需要实现简单的LRU缓存时。 需要确保解释清晰,逻辑连贯,避免技术术语过多导致理解困难。同时,结合源码分析时要抓住重点,展示关键代码部分并解释其作用,帮助用户理解内部机制。</think>### Java 集合LinkedHashMap 解析与源码分析 `LinkedHashMap` 是 `HashMap` 的子类,在保留 `HashMap` 高效查询能力的基础上,**通过双向链表维护元素的插入顺序或访问顺序**。它是实现 `LRU` 缓存策略的理想数据结构。 -- #### 核心特性 1. **有序性**:默认按插入顺序迭代,可配置为按访问顺序(最近访问的元素排在末尾)。 2. **性能**:查询、插入、删除的时间复杂度为 `O(1)`,略慢于 `HashMap`(需维护链表)。 3. **继承结构**:继承 `HashMap`,复用哈希表逻辑,重写链表维护相关方法。 --- ### 数据结构与关键源码 #### 1. Entry 结构扩展 `LinkedHashMap.Entry` 继承 `HashMap.Node`,**新增 `before` 和 `after` 指针**,形成双向链表。 ```java 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); } } ``` -- #### 2. 关键属性 - **head/tail**:维护链表的头尾节点。 - **accessOrder**:`true` 表示按访问顺序排序,`false`(默认)表示按插入顺序。 ```java transient LinkedHashMap.Entry<K,V> head; // 链表头(最老元素) transient LinkedHashMap.Entry<K,V> tail; // 链表尾(最新元素) final boolean accessOrder; // 顺序控制标志 ``` --- ### 核心方法源码分析 #### 1. 插入与链表维护 `LinkedHashMap` **未重写 `put()`**,直接复用 `HashMap` 的逻辑,但通过重写以下钩子方法维护链表: ```java // 插入新节点后,维护链表结构(可能触发删除最老元素) void afterNodeInsertion(boolean evict) { LinkedHashMap.Entry<K,V> first; if (evict && (first = head) != null && removeEldestEntry(first)) { removeNode(first.key, null, false, true); // 删除头节点(LRU 逻辑) } } // 是否删除最老元素(默认返回 false,需子类重写实现 LRU) protected boolean removeEldestEntry(Map.Entry<K,V> eldest) { return false; } ``` --- #### 2. 访问顺序调整 当 `accessOrder=true` 时,调用 `get()` 或 `getOrDefault()` 会触发 `afterNodeAccess()`,将节点移到链表末尾。 ```java public V get(Object key) { Node<K,V> e; if ((e = getNode(key)) == null) return null; if (accessOrder) afterNodeAccess(e); // 调整链表顺序 return e.value; } // 将节点移动到链表尾部(最近访问) void afterNodeAccess(Node<K,V> e) { LinkedHashMap.Entry<K,V> last = tail; if (accessOrder && last != e) { LinkedHashMap.Entry<K,V> p = (Entry<K,V>)e; LinkedHashMap.Entry<K,V> 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++; } } ``` --- #### 3. 迭代器实现 `LinkedHashMap` 的迭代器直接遍历双向链表,而非哈希桶数组,**保证顺序性**。 ```java abstract class LinkedHashIterator { LinkedHashMap.Entry<K,V> next = head; // 从头节点开始 LinkedHashMap.Entry<K,V> current; // ... public final boolean hasNext() { return next != null; } final LinkedHashMap.Entry<K,V> nextNode() { LinkedHashMap.Entry<K,V> e = next; // ... current = e; next = e.after; // 通过 after 指针遍历 return e; } } ``` -- ### 应用场景:实现 LRU 缓存 通过重写 `removeEldestEntry()`,限制最大容量并删除最久未使用的元素。 ```java public class LRUCache<K, V> extends LinkedHashMap<K, V> { private final int maxCapacity; public LRUCache(int maxCapacity) { super(maxCapacity, 0.75f, true); // accessOrder=true this.maxCapacity = maxCapacity; } @Override protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { return size() > maxCapacity; // 容量超限时删除最老元素 } } ``` --- ### 总结 - **有序性**:双向链表维护插入/访问顺序。 - **扩展性**:通过钩子方法(如 `afterNodeAccess`)实现行为定制。 - **高效性**:在 `HashMap` 基础上以少量性能代价换取顺序控制。 - **应用场景**:需要顺序迭代或实现类似 LRU 的缓存策略时优先选择。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值