图解LinkedHashMap数据结构设计与应用案例

445 阅读5分钟

image.png

LinkedHashMap 是 Java 中的一个 Map 实现,它继承自 HashMap 并添加了一个链表来维护键值对的插入顺序或者访问顺序。这意味着元素的迭代顺序可以是插入顺序或者最近最少使用(LRU)顺序,这取决于构造函数中的参数设置。LinkedHashMap 是非线程安全的,适用于需要保持插入顺序或访问顺序的场景,如实现 LRU 缓存。在性能上,它与 HashMap 类似,但在维护顺序上提供了额外的能力。

2.2 LinkedHashMap

LinkedHashMap 是 Java 中的一个 Map 实现,它继承自 HashMap 并添加了一个链表来维护元素的插入顺序或者访问顺序。

设计思考:
  1. 需求场景
    • 在许多应用中,不仅需要快速的查找性能,还需要保持元素的插入顺序或访问顺序。例如,实现一个 LRU(Least Recently Used)缓存,需要在元素被访问时更新其位置。
    • 适用于需要按插入顺序或访问顺序遍历键值对的场景,如会话管理、历史记录等。
  2. 现有技术局限性
    • HashMap 提供了快速的查找性能,但它不保证元素的顺序,即元素的迭代顺序是不确定的。
    • LinkedHashMap 的前身 LinkedHashMap(Java 1.4 之前)基于链表实现,虽然可以保持插入顺序,但查找性能较差,为 O(n)。
  3. 技术融合
    • LinkedHashMap 结合了 HashMap 的快速查找特性和链表的顺序保持功能,使用一个哈希表来存储键值对,同时使用一个双向链表来维护元素的插入或访问顺序。
  4. 设计理念
    • LinkedHashMap 旨在提供一个既能快速查找又能保持元素顺序的映射结构。它可以被配置为保持插入顺序或访问顺序。
  5. 实现方式
    • LinkedHashMap 内部使用一个哈希表来存储键值对,同时使用一个双向链表来维护元素的顺序。每个桶中的元素不再是单独的节点,而是包含在链表节点中的映射条目。
    • 当插入或访问元素时,可以根据需要更新链表,以保持插入顺序或访问顺序。
2.2.1 数据结构

image.png

图说明:
  • LinkedHashMap
    • 表示 LinkedHashMap 类的实例,是一个基于哈希表的 Map 实现,同时维护了元素的插入顺序或访问顺序。
  • Hash Table
    • LinkedHashMap 内部使用一个哈希表来存储键值对。
  • Buckets Array
    • 哈希表由一个桶数组组成,每个桶可以包含一个或多个节点(Node)。
  • Node1 & Node2
    • 表示桶中的两个具体节点,每个节点都包含键值对信息,并通过链表连接以维护顺序。
  • Key1 & Key2
    • 节点中存储的键。
  • Value1 & Value2
    • 节点中存储的值。
  • Next Node
    • 节点中的引用,指向链表中的下一个节点。
  • Prev Node
    • 节点中的引用,指向链表中的前一个节点,用于实现双向链表。
  • Head
    • 头指针,指向链表的第一个节点,即 LinkedHashMap 中的第一个元素。
  • Tail
    • 尾指针,指向链表的最后一个节点,即 LinkedHashMap 中的最后一个元素。
2.2.2 执行流程

image.png

图说明:
  • 创建 LinkedHashMap 实例:初始化 LinkedHashMap 对象。
  • 插入元素(put) :执行将键值对插入到 LinkedHashMap 的操作。
  • 计算键的哈希码:计算插入键的哈希码以确定其在桶数组中的位置。
  • 检查是否需要扩容:检查 LinkedHashMap 是否需要扩容(即桶数组的大小是否需要增加)。
  • 确定桶索引:根据哈希码和桶数组的大小确定桶索引。
  • 处理哈希冲突:如果桶中已有元素,则处理哈希冲突,可能是通过链表或红黑树。
  • 插入节点:将新节点插入到桶中。
  • 节点插入链表:将节点插入到链表的适当位置,以维护插入顺序。
  • 查找元素(get) :执行根据键查找值的操作。
  • 遍历桶:遍历桶中的链表或红黑树以查找节点。
  • 找到节点:找到包含指定键的节点。
  • 返回值:返回找到节点的值。
  • 删除元素(remove) :执行根据键删除键值对的操作。
  • 找到节点并删除:找到包含指定键的节点并从桶和链表中删除。
  • 更新链表:删除节点后,更新链表的链接。

优点

  1. 快速的查找性能
    • 继承自 HashMap,具有接近 O(1) 的平均时间复杂度的查找性能。
  2. 保持元素顺序
    • 可以保持元素的插入顺序或访问顺序,适合需要有序遍历的场景。
  3. 灵活的配置
    • 可以通过构造函数参数选择保持插入顺序或访问顺序。

缺点

  1. 内存开销
    • 相比于 HashMap,LinkedHashMap 需要额外的内存来维护双向链表。
  2. 对哈希冲突敏感
    • 在高冲突环境下,链表可能会变得较长,影响性能。

使用场景

  • 需要有序遍历的映射
    • 适用于需要按插入顺序或访问顺序遍历键值对的场景,如配置管理、历史记录等。
  • 实现 LRU 缓存
    • 适用于需要根据访问顺序淘汰元素的场景。

7、类设计

image.png

8、应用案例

LinkedHashMap 经常被用于实现缓存策略,特别是最近最少使用(LRU)缓存。以下是一个使用 LinkedHashMap 实现 LRU 缓存的案例:

import java.util.LinkedHashMap;
import java.util.Map;

public class LRUCache<K, V> extends LinkedHashMap<K, V> {
    private final int capacity;

    public LRUCache(int capacity) {
        // 设置一个适当的负载因子以减少 rehash 操作
        // 第三个参数 true 表示LinkedHashMap按照访问顺序来排序,最近访问的在头部,最老访问的在尾部
        super(capacity, 0.75f, true);
        this.capacity = capacity;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        // 当Map中数据量大于指定缓存个数的时候,返回true,自动删除最老的数据
        return size() > capacity;
    }

    public V get(Object key) {
        return super.get(key);
    }

    public V put(K key, V value) {
        return super.put(key, value);
    }

    public static void main(String[] args) {
        LRUCache<Integer, String> cache = new LRUCache<>(3);

        cache.put(1, "one");
        cache.put(2, "two");
        cache.put(3, "three");
        System.out.println("After put: " + cache);

        // 访问已存在的数据,会将该数据移动到末尾,表示最近访问
        cache.get(2);
        System.out.println("After get(2): " + cache);

        // 继续添加数据,此时最老的数据(key为1的数据)将被移除
        cache.put(4, "four");
        System.out.println("After put(4): " + cache);
    }
}