一句话总结
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. 插入节点
-
步骤:
- 像 HashMap 一样插入节点到哈希表中。
- 将新节点添加到链表末尾(维护插入顺序)。
-
源码关键点:
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))。
-
缺点:
- 内存开销:每个节点多存两个指针(空间换时间)。
- 非线程安全:多线程环境下需手动同步。
适用场景:
- 需要按顺序遍历 Map 的场景(如记录操作日志)。
- 实现 LRU 缓存淘汰策略。
口诀:
「LinkedHashMap 不一般,链表串联保顺序
插入访问两模式,LRU 缓存最合适!」