1. 概述

本文将深入探讨 LinkedHashMap 类的内部实现机制。作为 Map 接口最常用的实现之一,LinkedHashMap 继承自 HashMap,并共享其核心构建块。建议先熟悉 HashMap 实现原理 再继续阅读。

2. LinkedHashMap vs HashMap

LinkedHashMap 在大多数方面与 HashMap 高度相似,但通过结合哈希表和双向链表增强了功能:

  • ✅ 底层结构:默认容量为 16 的数组 + 贯穿所有条目的双向链表
  • ✅ 核心改造:扩展了 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);
    }
}

⚠️ 关键点:链表定义了迭代顺序,默认为插入顺序(insertion-order)

3. 插入顺序模式

当创建默认 LinkedHashMap 时,元素会严格按插入顺序排列,且在整个生命周期内保持该顺序:

@Test
public void givenLinkedHashMap_whenGetsOrderedKeyset_thenCorrect() {
    LinkedHashMap<Integer, String> map = new LinkedHashMap<>();
    map.put(1, null);
    map.put(2, null);
    map.put(3, null);
    map.put(4, null);
    map.put(5, null);

    Set<Integer> keys = map.keySet();
    Integer[] arr = keys.toArray(new Integer[0]);

    for (int i = 0; i < arr.length; i++) {
        assertEquals(new Integer(i + 1), arr[i]);
    }
}

✅ 测试保证:插入顺序始终维持(HashMap 无法保证此特性)
❌ 注意:键的重新插入不会影响已有顺序

4. 访问顺序模式

通过特殊构造函数可启用访问顺序(access-order)模式:

LinkedHashMap<Integer, String> map = new LinkedHashMap<>(16, .75f, true);

参数说明:

  1. 初始容量(16)
  2. 负载因子(0.75)
  3. 排序模式(true=访问顺序)

🔥 核心特性:迭代顺序变为"最近最少使用"(LRU)顺序,每次 put/get 都会更新访问顺序:

@Test
public void givenLinkedHashMap_whenAccessOrderWorks_thenCorrect() {
    LinkedHashMap<Integer, String> map 
      = new LinkedHashMap<>(16, .75f, true);
    map.put(1, null);
    map.put(2, null);
    map.put(3, null);
    map.put(4, null);
    map.put(5, null);

    Set<Integer> keys = map.keySet();
    assertEquals("[1, 2, 3, 4, 5]", keys.toString());
 
    map.get(4);  // 访问键4
    assertEquals("[1, 2, 3, 5, 4]", keys.toString());
 
    map.get(1);  // 访问键1
    assertEquals("[2, 3, 5, 4, 1]", keys.toString());
 
    map.get(3);  // 访问键3
    assertEquals("[2, 5, 4, 1, 3]", keys.toString());
}

⚠️ 重要:只有显式访问操作(如 get/put)会影响顺序,迭代操作不会

4.1 实现 LRU 缓存

通过重写 removeEldestEntry() 可自动淘汰最旧条目:

public class MyLinkedHashMap<K, V> extends LinkedHashMap<K, V> {
    private static final int MAX_ENTRIES = 5;

    public MyLinkedHashMap(
      int initialCapacity, float loadFactor, boolean accessOrder) {
        super(initialCapacity, loadFactor, accessOrder);
    }

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

测试效果(容量上限 5):

@Test
public void givenLinkedHashMap_whenRemovesEldestEntry_thenCorrect() {
    LinkedHashMap<Integer, String> map
      = new MyLinkedHashMap<>(16, .75f, true);
    map.put(1, null);
    map.put(2, null);
    map.put(3, null);
    map.put(4, null);
    map.put(5, null);
    Set<Integer> keys = map.keySet();
    assertEquals("[1, 2, 3, 4, 5]", keys.toString());
 
    map.put(6, null);  // 淘汰键1
    assertEquals("[2, 3, 4, 5, 6]", keys.toString());
 
    map.put(7, null);  // 淘汰键2
    assertEquals("[3, 4, 5, 6, 7]", keys.toString());
 
    map.put(8, null);  // 淘汰键3
    assertEquals("[4, 5, 6, 7, 8]", keys.toString());
}

5. 性能考量

操作 时间复杂度 与 HashMap 对比
基本操作 O(1) 略慢于 HashMap(链表维护开销)
迭代操作 O(n) 优于 HashMap(n=条目数)

关键差异:

  • LinkedHashMap 迭代时间仅与条目数相关
  • HashMap 迭代时间 = 容量 + 条目数(O(size+capacity))
  • ⚠️ 初始容量过高对 LinkedHashMap 的影响小于 HashMap

6. 并发处理

HashMap 相同,LinkedHashMap 非线程安全。多线程访问时需外部同步:

Map m = Collections.synchronizedMap(new LinkedHashMap());

⚠️ 特殊注意:在访问顺序模式下,get() 操作也属于结构性修改(与 put/remove 同级)

7. 总结

LinkedHashMap 通过维护双向链表实现了:

  1. ✅ 插入顺序保证
  2. ✅ 访问顺序支持(LRU 缓存基础)
  3. ✅ 迭代性能优化

选择建议:

  • 需要保留插入顺序 → 直接使用
  • 实现 LRU 缓存 → 访问顺序 + 重写 removeEldestEntry()
  • 高并发场景 → 外部同步或改用 ConcurrentHashMap

完整示例代码见 GitHub 项目


原始标题:A Guide to LinkedHashMap in Java