LinkedHashMap 是 Java 中的一个 Map 实现,它继承自 HashMap 并添加了一个链表来维护键值对的插入顺序或者访问顺序。这意味着元素的迭代顺序可以是插入顺序或者最近最少使用(LRU)顺序,这取决于构造函数中的参数设置。LinkedHashMap 是非线程安全的,适用于需要保持插入顺序或访问顺序的场景,如实现 LRU 缓存。在性能上,它与 HashMap 类似,但在维护顺序上提供了额外的能力。
2.2 LinkedHashMap
LinkedHashMap 是 Java 中的一个 Map 实现,它继承自 HashMap 并添加了一个链表来维护元素的插入顺序或者访问顺序。
设计思考:
- 需求场景:
- 在许多应用中,不仅需要快速的查找性能,还需要保持元素的插入顺序或访问顺序。例如,实现一个 LRU(Least Recently Used)缓存,需要在元素被访问时更新其位置。
- 适用于需要按插入顺序或访问顺序遍历键值对的场景,如会话管理、历史记录等。
- 现有技术局限性:
- HashMap 提供了快速的查找性能,但它不保证元素的顺序,即元素的迭代顺序是不确定的。
- LinkedHashMap 的前身 LinkedHashMap(Java 1.4 之前)基于链表实现,虽然可以保持插入顺序,但查找性能较差,为 O(n)。
- 技术融合:
- LinkedHashMap 结合了 HashMap 的快速查找特性和链表的顺序保持功能,使用一个哈希表来存储键值对,同时使用一个双向链表来维护元素的插入或访问顺序。
- 设计理念:
- LinkedHashMap 旨在提供一个既能快速查找又能保持元素顺序的映射结构。它可以被配置为保持插入顺序或访问顺序。
- 实现方式:
- LinkedHashMap 内部使用一个哈希表来存储键值对,同时使用一个双向链表来维护元素的顺序。每个桶中的元素不再是单独的节点,而是包含在链表节点中的映射条目。
- 当插入或访问元素时,可以根据需要更新链表,以保持插入顺序或访问顺序。
2.2.1 数据结构
图说明:
- 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 执行流程
图说明:
- 创建 LinkedHashMap 实例:初始化
LinkedHashMap对象。 - 插入元素(put) :执行将键值对插入到
LinkedHashMap的操作。 - 计算键的哈希码:计算插入键的哈希码以确定其在桶数组中的位置。
- 检查是否需要扩容:检查
LinkedHashMap是否需要扩容(即桶数组的大小是否需要增加)。 - 确定桶索引:根据哈希码和桶数组的大小确定桶索引。
- 处理哈希冲突:如果桶中已有元素,则处理哈希冲突,可能是通过链表或红黑树。
- 插入节点:将新节点插入到桶中。
- 节点插入链表:将节点插入到链表的适当位置,以维护插入顺序。
- 查找元素(get) :执行根据键查找值的操作。
- 遍历桶:遍历桶中的链表或红黑树以查找节点。
- 找到节点:找到包含指定键的节点。
- 返回值:返回找到节点的值。
- 删除元素(remove) :执行根据键删除键值对的操作。
- 找到节点并删除:找到包含指定键的节点并从桶和链表中删除。
- 更新链表:删除节点后,更新链表的链接。
优点
- 快速的查找性能:
- 继承自 HashMap,具有接近 O(1) 的平均时间复杂度的查找性能。
- 保持元素顺序:
- 可以保持元素的插入顺序或访问顺序,适合需要有序遍历的场景。
- 灵活的配置:
- 可以通过构造函数参数选择保持插入顺序或访问顺序。
缺点
- 内存开销:
- 相比于 HashMap,LinkedHashMap 需要额外的内存来维护双向链表。
- 对哈希冲突敏感:
- 在高冲突环境下,链表可能会变得较长,影响性能。
使用场景
- 需要有序遍历的映射:
- 适用于需要按插入顺序或访问顺序遍历键值对的场景,如配置管理、历史记录等。
- 实现 LRU 缓存:
- 适用于需要根据访问顺序淘汰元素的场景。
7、类设计
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);
}
}