JCSprout项目解析:深入理解LinkedHashMap实现原理

JCSprout项目解析:深入理解LinkedHashMap实现原理

JCSprout 👨‍🎓 Java Core Sprout : basic, concurrent, algorithm JCSprout 项目地址: https://gitcode.com/gh_mirrors/jc/JCSprout

前言

在Java集合框架中,HashMap是最常用的键值对存储结构,但它有一个明显的缺点:遍历顺序与插入顺序不一致。为了解决这个问题,Java提供了LinkedHashMap实现。本文将深入分析LinkedHashMap的实现原理,帮助开发者更好地理解和使用这个重要的集合类。

LinkedHashMap概述

LinkedHashMap是HashMap的一个子类,它在HashMap的基础上维护了一个双向链表,从而保证了元素的遍历顺序。这个顺序可以是:

  1. 插入顺序(默认):按照元素被添加到Map中的顺序
  2. 访问顺序:按照元素被访问的顺序(包括put和get操作)

这种特性使得LinkedHashMap非常适合需要保持元素顺序的场景,如实现LRU缓存。

核心数据结构

LinkedHashMap继承自HashMap,其核心数据结构包含两部分:

  1. 哈希表部分:继承自HashMap,使用数组+链表/红黑树的结构存储元素
  2. 双向链表部分:维护元素顺序的关键结构
private 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);
    }
}

每个Entry节点除了拥有HashMap.Node的基本属性外,还增加了before和after指针,用于维护双向链表结构。

顺序性实现原理

1. 插入顺序维护

当新元素插入时,LinkedHashMap会执行以下操作:

  1. 调用HashMap的put方法处理哈希表部分
  2. 将新节点添加到双向链表的尾部
void createEntry(int hash, K key, V value, int bucketIndex) {
    HashMap.Entry<K,V> old = table[bucketIndex];
    Entry<K,V> e = new Entry<>(hash, key, value, old);
    table[bucketIndex] = e;
    e.addBefore(header);  // 将新节点添加到链表尾部
    size++;
}

2. 访问顺序维护

当accessOrder为true时,每次访问元素(包括put和get)都会将该元素移动到链表尾部:

public V get(Object key) {
    Entry<K,V> e = (Entry<K,V>)getEntry(key);
    if (e == null)
        return null;
    e.recordAccess(this);  // 如果启用访问顺序,移动节点到尾部
    return e.value;
}

关键方法解析

初始化方法

LinkedHashMap重写了HashMap的init()方法,初始化双向链表头节点:

@Override
void init() {
    header = new Entry<>(-1, null, null, null);
    header.before = header.after = header;  // 循环链表
}

添加元素

LinkedHashMap重写了addEntry和createEntry方法:

void addEntry(int hash, K key, V value, int bucketIndex) {
    super.addEntry(hash, key, value, bucketIndex);
    // 可选的删除最老元素逻辑(用于实现LRU)
    Entry<K,V> eldest = header.after;
    if (removeEldestEntry(eldest)) {
        removeEntryForKey(eldest.key);
    }
}

删除元素

删除元素时,LinkedHashMap会同时从哈希表和双向链表中移除该节点:

void removeEntryForKey(Object key) {
    super.removeEntryForKey(key);
    // 从双向链表中移除节点的逻辑
}

性能特点

  1. 时间复杂度

    • 基本操作(get/put/remove):O(1)平均,O(n)最坏
    • 遍历操作:O(n),比HashMap略快(因为直接遍历链表)
  2. 空间复杂度

    • 比HashMap多维护一个双向链表,每个节点多两个指针的空间开销
  3. 线程安全性

    • 与HashMap一样,不是线程安全的

典型应用场景

  1. 维护插入顺序:需要按照元素添加顺序遍历时
  2. 实现LRU缓存:通过设置accessOrder=true和重写removeEldestEntry方法
  3. 需要可预测的迭代顺序:当HashMap的随机顺序不满足需求时

实现LRU缓存示例

public class LRUCache<K,V> extends LinkedHashMap<K,V> {
    private final int maxCapacity;
    
    public LRUCache(int maxCapacity) {
        super(maxCapacity, 0.75f, true);
        this.maxCapacity = maxCapacity;
    }
    
    @Override
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return size() > maxCapacity;
    }
}

与HashMap的对比

| 特性 | HashMap | LinkedHashMap | |---------------------|---------------|-------------------------| | 顺序性 | 无序 | 保持插入或访问顺序 | | 内部结构 | 数组+链表/红黑树 | 数组+链表/红黑树+双向链表 | | 迭代性能 | 较慢 | 较快 | | 内存占用 | 较少 | 较多 | | 适用场景 | 通用键值存储 | 需要顺序性的场景 |

总结

LinkedHashMap通过继承HashMap并添加双向链表的方式,实现了有序的Map结构。理解其实现原理有助于我们在适当场景下选择合适的数据结构,并能根据需要扩展其功能(如实现LRU缓存)。虽然它比HashMap有额外的内存开销,但在需要顺序性的场景下,这种开销通常是值得的。

JCSprout 👨‍🎓 Java Core Sprout : basic, concurrent, algorithm JCSprout 项目地址: https://gitcode.com/gh_mirrors/jc/JCSprout

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宣茹或

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值