详解:LinkedHashMap的工作原理和实现

119 阅读3分钟

LinkedHashMap 是 HashMap 的子类,通过双向链表维护键值对的插入顺序或访问顺序,结合了哈希表的快速访问与链表的顺序性。以下是其工作原理及实现细节的深入解析:

一、核心设计

1. 数据结构

  • 哈希表 + 双向链表
    继承 HashMap 的哈希桶结构,并在 Entry 节点中增加 前驱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);
        }
    }
    
  • 链表头尾指针

    transient LinkedHashMap.Entry<K,V> head; // 链表头(最早插入/访问的节点)
    transient LinkedHashMap.Entry<K,V> tail; // 链表尾(最新插入/访问的节点)
    

2. 顺序模式

  • 插入顺序(默认)
    节点按插入顺序排列,put 或 putAll 时新节点追加到链表尾部。
  • 访问顺序accessOrder=true
    每次调用 get 或 put 时,将访问的节点移动到链表尾部(最近访问),适合实现 LRU缓存

二、关键方法实现

1. 节点维护

  • 插入时维护链表
    重写 newNode 方法,创建带双向指针的 Entry,并链接到链表尾部:

    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); // 将p链接到链表尾部
        return p;
    }
    
  • 删除时解除链接
    重写 afterNodeRemoval 方法,在哈希表删除节点后调整链表指针:

    void afterNodeRemoval(Node<K,V> e) {
        LinkedHashMap.Entry<K,V> p = (Entry<K,V>)e;
        LinkedHashMap.Entry<K,V> 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;
    }
    

2. 访问顺序调整

当 accessOrder=true 时,访问节点会将其移至链表尾部:

void afterNodeAccess(Node<K,V> e) {
    LinkedHashMap.Entry<K,V> last = tail;
    if (accessOrder && last != e) {
        LinkedHashMap.Entry<K,V> p = (Entry<K,V>)e;
        LinkedHashMap.Entry<K,V> 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 tail = b;
        linkNodeLast(p); // 将p链接到链表尾部
    }
}

三、LRU缓存实现

1. 核心机制

  • 淘汰最久未使用节点
    设置 accessOrder=true,并覆盖 removeEldestEntry 方法定义淘汰策略:

    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return size() > MAX_CACHE_SIZE; // 当容量超限时移除最旧节点
    }
    

2. 完整示例

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

    public LRUCache() {
        super(16, 0.75f, true); // accessOrder=true
    }

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

四、性能分析

操作时间复杂度说明
get(key)O(1)哈希表访问 + 链表调整(访问顺序模式)
put(key, val)O(1)哈希表插入 + 链表维护
迭代遍历O(n)按链表顺序遍历,无需遍历哈希桶

五、与 HashMap 对比

特性LinkedHashMapHashMap
顺序性维护插入/访问顺序无顺序
内存占用更高(额外存储双向指针)较低
迭代性能更优(直接遍历链表)需遍历哈希桶
应用场景需要顺序性、LRU缓存纯快速键值存储

六、实现细节

  1. 迭代器优化
    直接按链表顺序遍历,无需扫描哈希桶空位置:

    abstract class LinkedHashIterator {
        LinkedHashMap.Entry<K,V> next = head; // 从头开始
        LinkedHashMap.Entry<K,V> current;
        // ...
    }
    
  2. 序列化处理
    仅序列化链表顺序,反序列化时重建哈希表和链表。

七、应用场景

  1. 保持插入顺序
    如需要按插入顺序遍历(如日志记录)。
  2. LRU缓存
    自动淘汰最久未使用的条目。
  3. 有序键值存储
    替代 TreeMap 的场景,若无需排序但需保持插入顺序。

八、注意事项

  1. 线程安全性
    非线程安全,多线程环境需使用 Collections.synchronizedMap 或 ConcurrentHashMap 包装。
  2. 内存开销
    每个节点多存储两个指针,内存占用高于 HashMap
  3. 初始化参数
    实现LRU时需合理设置初始容量和负载因子,避免频繁扩容和哈希冲突。

LinkedHashMap 通过哈希表与双向链表的结合,在保证高效查找的同时,提供了灵活的顺序管理能力,是Java集合框架中实现有序键值对存储的核心类之一。

更多分享

  1. 一文带你吃透Android中常见的高效数据结构
  2. 详解:ArrayMap和SparseArray在HashMap上面的改进
  3. 详解:HashMap与TreeMap、HashTable的区别
  4. 详解:Set集合是如何保证元素不重复的
  5. 一文带你搞懂HashSet和TreeSet的区别