一句话说透Java里面的LinkedHashMap工作原理和实现

196 阅读3分钟

一句话总结

LinkedHashMap 是「带链表的 HashMap」,它在 HashMap 的基础上,给每个键值对加了两个指针(前驱和后继),把这些节点串成一个双向链表。这个链表的作用是 记录插入顺序或访问顺序,所以遍历时能按顺序输出!


一、核心原理:双向链表维护顺序

1. 数据结构

  • 哈希表:底层和 HashMap 一样,用数组 + 链表/红黑树存储数据,实现快速查找(O(1) 时间复杂度)。
  • 双向链表:额外维护一个链表,记录节点的插入顺序或访问顺序。

2. 节点结构

LinkedHashMap 的节点类 Entry 继承自 HashMap 的 Node,并添加了前驱(before)和后继(after)指针:

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

二、两种排序模式

1. 插入顺序(默认)

  • 规则:节点按插入顺序排列。

  • 示例

    LinkedHashMap<String, Integer> map = new LinkedHashMap<>();
    map.put("A", 1);
    map.put("B", 2);
    map.put("C", 3);
    // 遍历输出顺序:A → B → C
    

2. 访问顺序

  • 规则:每次访问(get 或 put)一个节点,就把这个节点移到链表末尾。

  • 开启方式:构造函数中设置 accessOrder=true

  • 示例

    LinkedHashMap<String, Integer> map = new LinkedHashMap<>(16, 0.75f, true);
    map.put("A", 1);
    map.put("B", 2);
    map.get("A"); // 访问A,A被移到链表末尾
    // 遍历输出顺序:B → A
    

三、关键操作流程

1. 插入节点

  • 步骤

    1. 像 HashMap 一样插入节点到哈希表中。
    2. 将新节点添加到链表末尾(维护插入顺序)。
  • 源码关键点

    void afterNodeInsertion(boolean evict) {
        // 如果开启了移除最老节点的机制(如LRU缓存)
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            removeNode(first.key, null, false, true);
        }
    }
    

2. 访问节点

  • 访问顺序模式下:每次访问节点后,将其移到链表末尾。

  • 源码关键点

    void afterNodeAccess(Node<K,V> e) {
        if (accessOrder && last != e) {
            // 把节点e移到链表末尾
            LinkedHashMap.Entry<K,V> p = (Entry<K,V>)e;
            if (p.after != null) p.after.before = p.before;
            if (p.before != null) p.before.after = p.after;
            p.before = last;
            p.after = null;
            if (last != null) last.after = p;
            else head = p;
            last = p;
        }
    }
    

四、实现 LRU 缓存

1. 核心机制

  • 设置 accessOrder=true,使最近访问的节点排在链表末尾。
  • 移除最久未使用的节点:当容量满时,删除链表头节点。

2. 代码示例

public class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private final int capacity;

    public LRUCache(int capacity) {
        super(capacity, 0.75f, true); // 开启访问顺序模式
        this.capacity = capacity;
    }

    // 当元素数量超过容量时,移除最老的节点
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size() > capacity;
    }
}

// 使用示例
LRUCache<String, String> cache = new LRUCache<>(3);
cache.put("A", "1");
cache.put("B", "2");
cache.put("C", "3");
cache.get("A");       // 访问A,A被移到末尾
cache.put("D", "4");  // 容量超限,移除最老的B
// 最终内容:C → A → D

五、总结

  • 优点

    • 有序遍历:支持按插入顺序或访问顺序遍历。
    • 高效性:查找和插入的时间复杂度与 HashMap 相同(O(1))。
  • 缺点

    • 内存开销:每个节点多存两个指针(空间换时间)。
    • 非线程安全:多线程环境下需手动同步。

适用场景

  1. 需要按顺序遍历 Map 的场景(如记录操作日志)。
  2. 实现 LRU 缓存淘汰策略。

口诀
「LinkedHashMap 不一般,链表串联保顺序
插入访问两模式,LRU 缓存最合适!」