LinkedHashMap 源码深度分析(基于 JDK 8)

AI的出现,是否能替代IT从业者? 10w+人浏览 1.5k人参与

LinkedHashMap 源码深度分析(基于 JDK 8)

LinkedHashMap 是 Java 集合框架中兼具 哈希表高效查找双向链表有序迭代 的数据结构,其核心设计是 继承 HashMap 并额外维护双向链表,既保留了 HashMap O (1) 级别的增删改查性能,又解决了 HashMap 迭代无序的问题。同时,通过 accessOrder 开关支持 插入顺序访问顺序(LRU 缓存核心)两种模式。

一、类结构与核心成员

1. 继承关系

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
  • 直接继承 HashMap,复用其哈希表(数组 + 链表 / 红黑树)的核心逻辑;
  • 实现 Map 接口,遵循标准 Map 规范。

2. 节点结构(Entry)

LinkedHashMap 重写了 HashMap 的节点类,在 Node 基础上增加 双向链表指针before/after),同时保留哈希表的 hash/key/value/next 字段:

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);
    }
}
  • 哈希表维度:next 指针用于解决哈希冲突(链表 / 红黑树);
  • 双向链表维度:before(前驱)和 after(后继)指针用于维护顺序。

3. 核心成员变量

// 双向链表的头节点(最老节点:插入最早/最久未访问)
transient LinkedHashMap.Entry<K,V> head;
// 双向链表的尾节点(最新节点:插入最晚/最近访问)
transient LinkedHashMap.Entry<K,V> tail;
// 顺序模式开关:false=插入顺序(默认),true=访问顺序(LRU模式)
final boolean accessOrder;
  • headtail 控制双向链表的遍历和节点增删;
  • accessOrder 是 LinkedHashMap 功能分化的核心,默认 false(插入顺序)。

二、构造方法

LinkedHashMap 所有构造方法均 调用父类 HashMap 的构造方法,仅额外初始化 accessOrder(默认 false):

// 1. 无参构造:默认初始容量16,负载因子0.75,插入顺序
public LinkedHashMap() {
    super();
    accessOrder = false;
}

// 2. 指定初始容量
public LinkedHashMap(int initialCapacity) {
    super(initialCapacity);
    accessOrder = false;
}

// 3. 指定初始容量和负载因子
public LinkedHashMap(int initialCapacity, float loadFactor) {
    super(initialCapacity, loadFactor);
    accessOrder = false;
}

// 4. 传入Map初始化
public LinkedHashMap(Map<? extends K, ? extends V> m) {
    super();
    accessOrder = false;
    putMapEntries(m, false); // 复用HashMap的批量插入逻辑
}

// 5. 关键构造:支持指定顺序模式(accessOrder)
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder; // 唯一能开启LRU模式的构造
}
  • 核心差异:前 4 个构造默认 accessOrder=false(插入顺序),第 5 个构造可手动开启 accessOrder=true(访问顺序)。

三、核心逻辑:双向链表的维护

LinkedHashMap 的核心价值是 在 HashMap 基础上维护双向链表顺序,所有链表操作均通过重写 HashMap 的 “钩子方法” 和节点创建逻辑实现,不破坏 HashMap 的原有哈希表逻辑。

1. 节点创建:newNode(重写 HashMap 方法)

HashMap 插入节点时会调用 newNode 创建节点,LinkedHashMap 重写该方法,在创建节点后 自动将节点加入双向链表尾部

Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
    LinkedHashMap.Entry<K,V> p = new LinkedHashMap.Entry<>(hash, key, value, next);
    linkNodeLast(p); // 尾插法:将新节点加入双向链表尾部
    return p;
}

2. 尾插法:linkNodeLast(私有方法)

负责将新节点插入双向链表尾部,维护 headtail 指针:

private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    LinkedHashMap.Entry<K,V> last = tail; // 保存原尾节点
    tail = p; // 新节点成为尾节点
    if (last == null) { // 原链表为空(首次插入)
        head = p; // 新节点同时作为头节点
    } else { // 原链表非空
        p.before = last; // 新节点的前驱 = 原尾节点
        last.after = p; // 原尾节点的后继 = 新节点
    }
}
  • 插入顺序模式的核心:新节点永远追加到链表尾部,迭代时按插入顺序遍历。

3. 访问后调整:afterNodeAccess(钩子方法)

accessOrder=true(访问顺序模式)时,访问节点后需将该节点移到链表尾部(标记为 “最新访问”),这是 LRU 缓存的核心逻辑:

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;
        LinkedHashMap.Entry<K,V> b = p.before; // 前驱节点
        LinkedHashMap.Entry<K,V> a = p.after;  // 后继节点

        // 1. 断开p与后继a的连接
        p.after = null;
        if (b == null) { // p原是头节点(无前驱)
            head = a; // 后继a成为新头节点
        } else { // p有前驱
            b.after = a; // 前驱的后继指向a
        }

        // 2. 断开p与前驱b的连接
        if (a != null) { // a存在(p不是尾节点)
            a.before = b; // a的前驱指向b
        } else { // a不存在(p原是倒数第二个节点)
            last = b; // 原前驱b成为临时尾节点
        }

        // 3. 将p插入到链表尾部
        if (last == null) { // 链表为空(极端情况)
            head = p;
        } else {
            p.before = last; // p的前驱 = 临时尾节点
            last.after = p; // 临时尾节点的后继 = p
        }
        tail = p; // p成为新尾节点
        ++modCount; // 迭代器快速失败标记
    }
}
  • 触发时机:调用 get()getOrDefault() 等访问方法时,会先通过 HashMap 的 getNode() 找到节点,再调用该方法调整顺序。

4. 插入后清理:afterNodeInsertion(钩子方法)

HashMap 插入节点成功后会回调该方法,LinkedHashMap 重写它实现 LRU 缓存的 “淘汰策略”:当 removeEldestEntry() 返回 true 时,删除链表头节点(最老未访问节点):

void afterNodeInsertion(boolean evict) { // evict:是否允许淘汰(put时为true)
    LinkedHashMap.Entry<K,V> first;
    // 条件:允许淘汰 + 存在头节点(最老节点) + 满足淘汰规则
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true); // 删除头节点(哈希表+链表)
    }
}

5. 淘汰规则:removeEldestEntry(可重写方法)

默认返回 false(不淘汰任何节点),要实现 LRU 缓存需重写该方法,定义淘汰条件(如 “容量超过阈值时淘汰最老节点”):

protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
    return false; // 默认不淘汰
}
  • 示例:重写后实现固定容量的 LRU 缓存:

    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; // 容量超限时,淘汰最老节点
        }
    }
    

6. 删除后调整:afterNodeRemoval(钩子方法)

HashMap 删除节点成功后会回调该方法,LinkedHashMap 重写它以 从双向链表中移除被删除的节点,保证链表结构完整:

void afterNodeRemoval(Node<K,V> e) { // e:被删除的节点
    LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e;
    LinkedHashMap.Entry<K,V> b = p.before; // 前驱
    LinkedHashMap.Entry<K,V> a = p.after;  // 后继

    // 断开p的连接(帮助GC)
    p.before = null;
    p.after = null;

    // 调整前驱和后继的连接
    if (b == null) { // p原是头节点
        head = a; // 后继a成为新头节点
    } else {
        b.after = a;
    }

    if (a == null) { // p原是尾节点
        tail = b; // 前驱b成为新尾节点
    } else {
        a.before = b;
    }
}

四、迭代器与顺序保证

LinkedHashMap 的迭代器 基于双向链表实现,而非哈希表,因此迭代顺序严格遵循链表顺序(插入 / 访问顺序):

  • keySet()entrySet()values() 返回的集合迭代器,均通过遍历 head → ... → tail 实现;
  • 迭代时间复杂度 O (n),且顺序稳定(HashMap 迭代顺序随机)。

核心迭代逻辑示例(以 entrySet() 为例):

public Set<Map.Entry<K,V>> entrySet() {
    Set<Map.Entry<K,V>> es;
    return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
}

private class LinkedEntrySet extends AbstractSet<Map.Entry<K,V>> {
    public Iterator<Map.Entry<K,V>> iterator() {
        return new LinkedEntryIterator();
    }
}

private class LinkedEntryIterator extends LinkedHashIterator implements Iterator<Map.Entry<K,V>> {
    public Map.Entry<K,V> next() {
        return nextNode(); // 遍历双向链表
    }
}

abstract class LinkedHashIterator {
    LinkedHashMap.Entry<K,V> next; // 下一个节点
    LinkedHashMap.Entry<K,V> current; // 当前节点
    int expectedModCount; // 快速失败标记

    LinkedHashIterator() {
        next = head; // 迭代从链表头节点开始
        expectedModCount = modCount;
    }

    public final boolean hasNext() {
        return next != null;
    }

    final LinkedHashMap.Entry<K,V> nextNode() {
        LinkedHashMap.Entry<K,V> e = next;
        if (modCount != expectedModCount) {
            throw new ConcurrentModificationException(); // 快速失败
        }
        if (e == null) {
            throw new NoSuchElementException();
        }
        current = e;
        next = e.after; // 按链表后继节点遍历
        return e;
    }

    public final void remove() { ... } // 调用afterNodeRemoval
}

五、性能与线程安全

1. 性能特点

  • 时间复杂度:增删改查均为 O (1)(与 HashMap 一致),仅额外增加双向链表的指针操作(O (1) 开销);
  • 空间开销:每个节点比 HashMap 多 2 个指针(before/after),内存占用略高;
  • 迭代效率:迭代时无需遍历哈希表空桶,效率比 HashMap 更稳定(尤其当负载因子较高时)。

2. 线程安全

  • 与 HashMap 一致,线程不安全:多线程并发修改(如同时 put/remove)可能导致链表结构破坏、迭代器快速失败(ConcurrentModificationException);
  • 解决方案:使用 Collections.synchronizedMap(new LinkedHashMap<>()) 包装,或使用并发容器 ConcurrentLinkedHashMap(Guava 提供)。

六、与 HashMap 的核心区别

特性HashMapLinkedHashMap
底层结构哈希表(数组 + 链表 / 红黑树)哈希表 + 双向链表
迭代顺序无序(随机)有序(插入顺序 / 访问顺序)
节点结构Node(hash/key/value/next)Entry(继承 Node + before/after)
LRU 支持不支持支持(accessOrder=true)
空间开销较低较高(多 2 个指针)
迭代效率不稳定(需跳过空桶)稳定(直接遍历链表)

七、典型应用场景

  1. 需要有序迭代的 Map:如日志记录、历史操作记录(插入顺序);
  2. LRU 缓存:如内存缓存、会话缓存(访问顺序,重写 removeEldestEntry 控制容量);
  3. 频繁迭代的场景:比 HashMap 迭代效率更稳定。

总结

LinkedHashMap 的核心设计是 “哈希表保证效率,双向链表保证顺序”

  • 继承 HashMap 复用哈希表的 O (1) 增删改查能力;
  • 通过双向链表维护顺序,支持插入 / 访问两种模式;
  • 钩子方法(afterNodeAccess/afterNodeInsertion)实现 LRU 缓存逻辑,扩展性强。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值