LinkedHashMap实现有序的原理

LinkedHashMap实现有序的详细步骤

LinkedHashMap是Java集合框架中的一个重要类,它通过特定的实现方式保证了元素的遍历顺序与插入顺序一致(或访问顺序)。以下是LinkedHashMap实现有序的详细步骤和原理:

1. 数据结构基础

LinkedHashMap继承自HashMap,在HashMap的基础上进行了扩展:

public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>

2. 维护双向链表

LinkedHashMap在HashMap的数组+链表/红黑树结构基础上,额外维护了一个双向链表:

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

3. 实现有序的关键字段

LinkedHashMap类中定义了以下重要字段:

// 双向链表的头节点(最老的节点)
transient LinkedHashMap.Entry<K,V> head;

// 双向链表的尾节点(最新的节点)
transient LinkedHashMap.Entry<K,V> tail;

// 排序模式:true-访问顺序,false-插入顺序
final boolean accessOrder;

4. 插入顺序维护机制

当插入新元素时,LinkedHashMap会执行以下步骤:

  1. ​调用父类HashMap的put方法​​:完成基本的哈希表插入操作
  2. ​创建LinkedHashMap.Entry节点​​:而不是普通的HashMap.Node
  3. ​链接到双向链表尾部​​:
    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;
        }
    }

5. 访问顺序维护机制(当accessOrder=true时)

当访问一个已存在的元素时(get或put更新),LinkedHashMap会:

  1. ​移动访问的节点到链表尾部​​:
    void afterNodeAccess(Node<K,V> 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)
                head = a;
            else
                b.after = a;
            if (a != null)
                a.before = b;
            else
                last = b;
            if (last == null)
                head = p;
            else {
                p.before = last;
                last.after = p;
            }
            tail = p;
            ++modCount;
        }
    }

6. 迭代顺序保证

LinkedHashMap的迭代器直接遍历双向链表而非哈希表:

abstract class LinkedHashIterator {
    LinkedHashMap.Entry<K,V> next;
    LinkedHashMap.Entry<K,V> current;
    
    LinkedHashIterator() {
        next = head; // 从头节点开始
        current = null;
    }
    
    final LinkedHashMap.Entry<K,V> nextNode() {
        LinkedHashMap.Entry<K,V> e = next;
        if (e == null)
            throw new NoSuchElementException();
        current = e;
        next = e.after; // 沿着链表向后遍历
        return e;
    }
    // ... 其他方法
}

7. 删除节点时的链表维护

当删除节点时,LinkedHashMap会同时更新双向链表:

void afterNodeRemoval(Node<K,V> e) {
    LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
    p.before = p.after = null;
    if (b == null)
        head = a;
    else
        b.after = a;
    if (a == null)
        tail = b;
    else
        a.before = b;
}

总结

LinkedHashMap通过以下方式实现有序:

  1. 继承HashMap的所有功能
  2. 使用双向链表维护所有节点的顺序
  3. 在节点插入、访问和删除时维护链表结构
  4. 迭代时直接遍历双向链表而非哈希表

这种实现方式既保留了HashMap的高效查找特性,又提供了可预测的迭代顺序。

你提供的代码片段是 LinkedHashMap 中的一个关键方法 afterNodeAccess(Node<K,V> e),它负责在​​访问顺序模式(accessOrder=true)​​下维护双向链表的顺序。下面我将详细解释这段代码的逻辑和作用。


​方法作用​

accessOrder=true 时(即按访问顺序排序),每次访问(get()put() 更新)某个节点时,会调用 afterNodeAccess(e),将该节点移动到双向链表的​​尾部​​(表示它是最近访问的节点)。

​为什么移动节点到尾部?​

  • ​LRU(Least Recently Used)缓存策略​​:尾部表示最近访问,头部表示最久未访问。
  • ​迭代顺序​​:遍历 LinkedHashMap 时,会从头部(最老)到尾部(最新)顺序访问。

​代码逐行解析​

void afterNodeAccess(Node<K,V> e) {  // e 是被访问的节点
    LinkedHashMap.Entry<K,V> last;   // 用于记录当前尾节点
    if (accessOrder && (last = tail) != e) {  // 1. 检查是否需要调整顺序
        LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e;  // 2. 强制转换为Entry(带before/after指针)
        LinkedHashMap.Entry<K,V> b = p.before; // 3. 前驱节点
        LinkedHashMap.Entry<K,V> a = p.after;  // 4. 后继节点
        p.after = null;  // 5. 断开当前节点的后继指针(准备移动)

        // 6. 处理前驱节点(b)
        if (b == null)
            head = a;    // 如果前驱是null,说明e是头节点,新头节点变成a
        else
            b.after = a; // 否则,前驱节点直接指向后继节点(跳过e)

        // 7. 处理后继节点(a)
        if (a != null)
            a.before = b; // 如果后继存在,它的前驱指向b(跳过e)
        else
            last = b;     // 如果后继是null,说明e是尾节点,last暂存b

        // 8. 将e移动到链表尾部
        if (last == null)
            head = p;    // 链表为空(理论上不会发生)
        else {
            p.before = last;  // e的前驱指向原尾节点
            last.after = p;   // 原尾节点的后继指向e
        }
        tail = p;        // 更新尾节点为e
        ++modCount;      // 修改计数器(用于fail-fast迭代)
    }
}

​操作流程(图示)​

假设双向链表当前状态:
head → A → B → C → tailaccessOrder=true

  1. ​访问节点 B​​(调用 afterNodeAccess(B)):

    • 断开 B 的前后连接:A → C
    • B 移动到尾部:A → C → B
    • 新链表:head → A → C → B → tail
  2. ​再次访问 A​​:

    • 断开 A 的连接:head → C → B
    • 移动 A 到尾部:C → B → A
    • 新链表:head → C → B → A → tail

​关键点总结​

  1. ​条件检查​

    • 仅当 accessOrder=true 且当前节点不是尾节点时,才需要调整。
  2. ​链表操作​

    • 将节点 e 从原位置移除(处理前驱和后继的指针)。
    • e 插入到链表尾部(使其成为最新访问的节点)。
  3. ​LRU 应用​

    • 结合 removeEldestEntry() 方法,可实现 LRU 缓存淘汰策略(淘汰头节点)。

​示例场景​

LinkedHashMap<Integer, String> map = new LinkedHashMap<>(16, 0.75f, true);
map.put(1, "A");
map.put(2, "B");
map.put(3, "C");

map.get(2); // 调用afterNodeAccess,链表顺序变为:1 → 3 → 2
map.get(1); // 顺序变为:3 → 2 → 1

通过这种方式,LinkedHashMapaccessOrder=true 时,能始终保持​​最近访问的节点在尾部​​,从而实现 LRU 行为。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值