LinkedHashMap 源码解析
简介
LinkedHashMap 继承自 HashMap,在 HashMap 的基础上维护了一条贯穿所有节点的双向链表,从而保证迭代顺序是可预测的。它与 HashMap 的关系可以概括为:HashMap 负责「查找」,LinkedHashMap 额外负责「维护顺序」。
- 插入顺序(默认):
accessOrder = false时,迭代顺序就是键值对的插入顺序。 - 访问顺序:
accessOrder = true时,每次get/put已存在的 key 都会把该节点移到链表尾部,迭代顺序反映「最近最少使用(LRU)」的访问情况,因此非常适合实现 LRU 缓存。
LinkedHashMap 同样不是线程安全的;多线程环境下可以用 Collections.synchronizedMap 包装,或改用 ConcurrentHashMap。
注意:与
HashMap一样,允许null键和null值;初始容量、负载因子等概念完全继承自HashMap,对性能的影响也一致。
底层数据结构分析
LinkedHashMap 的核心思路是:在 HashMap 的「数组 + 链表/红黑树」结构之外,再用 before/after 指针把所有节点串成一条双向链表。
// LinkedHashMap 的节点,继承自 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);
}
}除了 HashMap 里的 table、size、modCount 等字段外,LinkedHashMap 自己只多了三个关键字段:
// 双向链表的头节点(最久的节点)
transient LinkedHashMap.Entry<K,V> head;
// 双向链表的尾节点(最新的节点)
transient LinkedHashMap.Entry<K,V> tail;
// 迭代顺序模式:false=插入顺序,true=访问顺序
final boolean accessOrder;这样,同一个 Entry 同时存在于两处:
HashMap的桶(bucket)里,通过next指针解决哈希冲突(链表/红黑树);LinkedHashMap维护的双向链表里,通过before/after指针记录顺序。
因此 LinkedHashMap 的 get、遍历操作的时间复杂度与 HashMap 一致(理想 O(1)),只是每个节点多维护两个指针,内存占用略高。
源码分析
构造方法
LinkedHashMap 提供了多个构造方法,核心区别在于 accessOrder 的默认值。
// 默认构造:插入顺序
public LinkedHashMap() {
super();
accessOrder = false;
}
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
// 唯一可以指定 accessOrder 的构造方法
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
// 从另一个 Map 构造,默认插入顺序
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super();
accessOrder = false;
putMapEntries(m, false);
}节点接入:newNode 与 linkNodeLast
HashMap 在新增节点时调用 newNode(),LinkedHashMap 重写它,在创建节点的同时把节点链接到双向链表的尾部。
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;
}
// 红黑树节点同理
TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
TreeNode<K,V> p = new TreeNode<>(hash, key, value, next);
linkNodeLast(p);
return p;
}
// 把节点 p 接到双向链表尾部
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;
tail = p;
if (last == null) // 链表为空,head 也指向 p
head = p;
else { // 否则挂到原 tail 后面
p.before = last;
last.after = p;
}
}三个回调钩子(核心)
HashMap 在 putVal、remove、clear 等操作中会调用三个「空实现」的回调方法:afterNodeAccess、afterNodeInsertion、afterNodeRemoval。LinkedHashMap 正是通过重写这三个方法来维护双向链表的顺序。
1. afterNodeAccess:把节点移到链表尾部(访问顺序的关键)
void afterNodeAccess(Node<K,V> e) { // 把节点 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) // e 原本是头节点
head = a;
else
b.after = a;
if (a != null) // e 不是尾节点
a.before = b;
else // e 原本是尾节点
last = b;
if (last == null) // 链表只剩 e 一个节点
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}当 accessOrder = true 时,get() 以及 put() 覆盖已存在 key 都会触发 afterNodeAccess,从而把刚访问的节点移动到尾部。配合 afterNodeInsertion 就能实现「淘汰最久未使用节点」的 LRU 语义。
2. afterNodeInsertion:插入后可能淘汰最老节点
void afterNodeInsertion(boolean evict) { // 可能移除最老的节点
LinkedHashMap.Entry<K,V> first;
// evict=true 且头节点存在,且 removeEldestEntry 返回 true 时淘汰
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}3. afterNodeRemoval:节点从 HashMap 移除时同步从链表摘除
void afterNodeRemoval(Node<K,V> e) { // 把节点 e 从双向链表中解除
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.before = p.after = null; // 断开 e 自身的指针,方便 GC
if (b == null) // e 是头节点
head = a;
else
b.after = a;
if (a == null) // e 是尾节点
tail = b;
else
a.before = b;
}get 方法(访问顺序的体现)
LinkedHashMap 重写了 get,在 HashMap.getNode 找到节点后,如果开启了访问顺序,会调用 afterNodeAccess 把节点移到尾部。
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;
}
// 类似地,getOrDefault 也会在访问顺序模式下触发 afterNodeAccess
@Override
public V getOrDefault(Object key, V defaultValue) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return defaultValue;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}containsValue(按链表顺序遍历)
HashMap.containsValue 需要逐个桶扫描,而 LinkedHashMap 直接沿着双向链表从头到尾遍历即可,代码更简洁,且天然按维护的顺序进行。
public boolean containsValue(Object value) {
// 沿双向链表 head -> after 顺序遍历,而非扫描 table
for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after) {
V v = e.value;
if (v == value || (value != null && value.equals(v)))
return true;
}
return false;
}removeEldestEntry(实现 LRU 缓存的开关)
removeEldestEntry 默认永远返回 false,即不自动淘汰。子类(如 LRU 缓存)重写它即可在容量超限时淘汰最老节点。
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false; // 默认不淘汰;LRU 缓存可重写为 size() > maxSize
}clear
清空时除了调用父类逻辑,还要把双向链表的头尾指针置空。
public void clear() {
super.clear();
head = tail = null;
}与 HashMap 的关键区别
| 维度 | HashMap | LinkedHashMap |
|---|---|---|
| 迭代顺序 | 无序(受哈希影响) | 插入顺序 或 访问顺序 |
| 内存占用 | 较少 | 每个节点多两个指针,略大 |
| get 性能 | O(1) | O(1),访问顺序下多一次链表移动 |
| 典型用途 | 一般键值存储 | 需要稳定迭代顺序、LRU 缓存 |
经典 Demo
import java.util.LinkedHashMap;
import java.util.Map;
public class LinkedHashMapDemo {
public static void main(String[] args) {
// 1. 插入顺序(accessOrder 默认 false)
LinkedHashMap<String, Integer> map = new LinkedHashMap<>();
map.put("a", 1);
map.put("b", 2);
map.put("c", 3);
System.out.println(map); // {a=1, b=2, c=3}
// 2. 访问顺序(accessOrder = true)
LinkedHashMap<String, Integer> lru = new LinkedHashMap<>(16, 0.75f, true);
lru.put("a", 1);
lru.put("b", 2);
lru.put("c", 3);
lru.get("a"); // 访问 a,a 被移到队尾
System.out.println(lru); // {b=2, c=3, a=1}
// 3. 基于 LinkedHashMap 实现固定容量的 LRU 缓存
LruCache<Integer, String> cache = new LruCache<>(3);
cache.put(1, "one");
cache.put(2, "two");
cache.put(3, "three");
cache.get(1); // 访问 1,1 移到队尾
cache.put(4, "four"); // 超过容量,淘汰最久未使用的 2
System.out.println(cache); // {3=three, 1=one, 4=four}
}
// 固定容量的 LRU 缓存:重写 removeEldestEntry 即可
static class LruCache<K, V> extends LinkedHashMap<K, V> {
private final int maxSize;
public LruCache(int maxSize) {
super(maxSize, 0.75f, true); // accessOrder = true
this.maxSize = maxSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > maxSize; // 超过容量就淘汰最老节点
}
}
}