一文搞懂 LinkedHashMap 底层原理

LinkedHashMap 是 HashMap 的“增强版”,核心是在 HashMap 基础上增加了双向链表,既保留了 HashMap 的高效查找特性,又能维护键值对的插入顺序/访问顺序。本文从核心结构、底层原理、核心方法到使用场景,帮你彻底搞懂它。

一、先明确核心定位

LinkedHashMap 继承自 HashMap,实现了 Map 接口,它的核心特点:

  1. 底层基础:复用 HashMap 的“数组+链表+红黑树”结构,哈希表部分完全继承 HashMap 的逻辑;
  2. 核心增强:额外维护一条双向链表,记录所有键值对的访问顺序;
  3. 有序性:支持两种顺序(默认插入顺序):
    • 插入顺序:按键值对 put 的先后顺序排列;
    • 访问顺序:按键值对被 get/put 访问的先后顺序排列(最近访问的在链表尾部)。

二、核心结构:HashMap + 双向链表

1. 底层数据结构拆解

LinkedHashMap 的底层是“哈希表 + 双向链表”的组合,我们先看关键的内部类和核心字段:

(1)核心内部类:Entry(节点)

LinkedHashMap 重写了 HashMap 的 Node 节点,增加了 beforeafter 指针,用于构建双向链表:

// LinkedHashMap 的节点类,继承自 HashMap.Node
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 的 Node 节点(只有 next 指针,单向链表),LinkedHashMap 的 Entry 多了 before/after,既能参与哈希表的链表/红黑树,又能参与双向链表。

(2)核心字段

LinkedHashMap 新增了 3 个核心字段,用于维护双向链表:

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V> {
    // 双向链表的头节点(最老的节点)
    transient LinkedHashMap.Entry<K,V> head;
    // 双向链表的尾节点(最新的节点)
    transient LinkedHashMap.Entry<K,V> tail;
    // 顺序模式:true=访问顺序,false=插入顺序(默认)
    final boolean accessOrder;
}

2. 结构可视化

用一张图理解 LinkedHashMap 的整体结构:

哈希表(数组+链表):
索引0: null
索引1: Entry1(hash, k1, v1, next=null) —— 同时属于双向链表
索引2: Entry2(hash, k2, v2, next=Entry3)
       ↓
       Entry3(hash, k3, v3, next=null)
...

双向链表:
head → Entry1 → Entry2 → Entry3 → tail
       ↑    ↓    ↑    ↓    ↑    ↓
      before after before after before after
  • 哈希表负责快速查找(O(1) 时间复杂度);
  • 双向链表负责维护顺序,遍历的时候直接走双向链表,保证顺序性。

三、核心原理:关键方法的实现逻辑

LinkedHashMap 几乎没有重写 HashMap 的 put/get 核心方法,而是通过重写 HashMap 的钩子方法(回调方法)来维护双向链表,这是它的精髓。

1. HashMap 的钩子方法(LinkedHashMap 的核心重写点)

HashMap 中定义了 3 个空的钩子方法,供子类扩展:

// HashMap 中的钩子方法,供 LinkedHashMap 重写
// 节点插入后调用
void afterNodeInsertion(boolean evict) {}
// 节点访问后调用(get/put 已存在的节点)
void afterNodeAccess(Node<K,V> p) {}
// 节点删除后调用
void afterNodeRemoval(Node<K,V> p) {}

LinkedHashMap 正是通过重写这 3 个方法,实现双向链表的维护。

2. 插入元素(put):维护插入顺序

当调用 put(K key, V value) 时,流程如下:

  1. 调用 HashMap 的 put 方法,完成哈希表的插入(数组/链表/红黑树的插入);
  2. 触发 HashMap 的 afterNodeInsertion 钩子方法,LinkedHashMap 重写该方法,将新节点添加到双向链表的尾部
  3. 如果是访问顺序模式(accessOrder=true),还会触发 afterNodeAccess,把当前节点移到双向链表尾部。

核心代码(LinkedHashMap 重写的 newNodeafterNodeInsertion):

// 重写创建节点的方法,创建 LinkedHashMap.Entry(带 before/after)
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    LinkedHashMap.Entry<K,V> p =
        new LinkedHashMap.Entry<>(hash, key, value, e);
    // 将新节点添加到双向链表尾部
    linkNodeLast(p);
    return p;
}

// 把节点链接到双向链表尾部
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    LinkedHashMap.Entry<K,V> last = tail;
    tail = p;
    if (last == null) // 链表为空,头节点指向当前节点
        head = p;
    else { // 链表非空,当前节点的before指向原尾节点,原尾节点的after指向当前节点
        p.before = last;
        last.after = p;
    }
}

3. 访问元素(get):维护访问顺序

当调用 get(K key) 时,流程如下:

  1. 调用 HashMap 的 get 方法,找到对应的节点(哈希表查找);
  2. 如果是访问顺序模式(accessOrder=true),触发 afterNodeAccess 钩子方法,将该节点移到双向链表的尾部(表示最近访问);
  3. 返回节点的 value。

核心代码(LinkedHashMap 重写的 getafterNodeAccess):

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

// 访问节点后,将节点移到双向链表尾部
void afterNodeAccess(Node<K,V> e) { 
    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;
    }
}

4. 删除元素(remove):维护双向链表

当调用 remove(K key) 时:

  1. 调用 HashMap 的 remove 方法,从哈希表中删除节点;
  2. 触发 afterNodeRemoval 钩子方法,LinkedHashMap 重写该方法,将节点从双向链表中移除。

核心代码:

void afterNodeRemoval(Node<K,V> e) { // 移除节点时,调整双向链表
    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;
}

5. LRU 缓存的核心:removeEldestEntry 方法

LinkedHashMap 提供了 removeEldestEntry 方法,默认返回 false,子类可以重写该方法实现LRU(最近最少使用)缓存:当链表长度超过阈值时,自动删除最老的节点(双向链表的头节点)。

示例代码(实现 LRU 缓存):

// 自定义 LinkedHashMap 实现 LRU 缓存,最多存储3个元素
class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private final int MAX_CAPACITY;

    public LRUCache(int maxCapacity) {
        // 初始化:初始容量16,负载因子0.75,访问顺序模式
        super(16, 0.75f, true);
        this.MAX_CAPACITY = maxCapacity;
    }

    // 重写该方法:当元素数量超过阈值时,删除最老的节点(头节点)
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > MAX_CAPACITY;
    }
}

// 测试 LRU 缓存
public class LRUCacheTest {
    public static void main(String[] args) {
        LRUCache<Integer, String> cache = new LRUCache<>(3);
        cache.put(1, "A");
        cache.put(2, "B");
        cache.put(3, "C");
        System.out.println(cache); // {1=A, 2=B, 3=C}

        cache.get(1); // 访问1,移到尾部
        System.out.println(cache); // {2=B, 3=C, 1=A}

        cache.put(4, "D"); // 超过容量,删除最老的2
        System.out.println(cache); // {3=C, 1=A, 4=D}
    }
}

四、核心特性与使用场景

1. 核心特性

特性 说明
有序性 支持插入顺序/访问顺序,遍历顺序与维护的顺序一致
查找性能 与 HashMap 一致,O(1)(哈希表查找)
遍历性能 比 HashMap 快(直接遍历双向链表,无需遍历哈希表的空桶)
线程安全性 非线程安全(和 HashMap 一样),多线程场景需用 ConcurrentHashMap 或加锁
允许 null 键/值 与 HashMap 一致,允许一个 null 键、多个 null 值

2. 典型使用场景

  1. 需要保留插入顺序的键值对场景
    • 记录用户操作日志(按操作顺序存储);
    • 配置项的有序存储(按配置加载顺序读取)。
  2. LRU 缓存实现
    • 本地缓存(如用户会话缓存、热点数据缓存);
    • 有限容量的缓存,自动淘汰最近最少使用的元素。
  3. 有序遍历的场景
    • 需要按插入/访问顺序遍历键值对(如排行榜、最近访问列表)。

五、与 HashMap 的核心区别

对比维度 HashMap LinkedHashMap
底层结构 数组+链表+红黑树 数组+链表+红黑树+双向链表
顺序性 无序(按哈希值排列) 有序(插入/访问顺序)
遍历性能 较慢(需遍历空桶) 较快(直接遍历双向链表)
内存占用 较小 较大(多维护双向链表)
核心用途 快速查找、存储 有序存储、LRU缓存

总结

  1. 核心结构:LinkedHashMap = HashMap(哈希表) + 双向链表(维护顺序),通过重写 HashMap 的钩子方法实现链表维护;
  2. 顺序控制:默认按插入顺序,设置 accessOrder=true 按访问顺序(最近访问的在链表尾部);
  3. 核心价值:既保留 HashMap 的高效查找,又解决了 HashMap 无序的问题,是实现 LRU 缓存的最佳选择(无需自己维护顺序)。

记住核心逻辑:哈希表负责快查,双向链表负责有序,钩子方法负责链表维护

posted @ 2026-03-06 11:00  七星6609  阅读(2)  评论(0)    收藏  举报