一、引言
在 Java 集合框架中,LinkedHashMap
是一个特殊且实用的类。它继承自 HashMap
,同时又维护了一个双向链表,这使得它不仅能像 HashMap
一样快速存储和查找键值对,还能按照插入顺序或者访问顺序来维护元素。本文将深入探讨 LinkedHashMap
的原理,包括其底层数据结构、核心属性、构造方法、常用操作的实现细节,并结合丰富的代码示例进行说明。
二、LinkedHashMap 概述
2.1 定义与用途
LinkedHashMap
是 java.util
包下的一个类,它实现了 Map
接口。与 HashMap
不同的是,LinkedHashMap
能够记住元素的插入顺序或者访问顺序。这一特性使得它在很多场景下非常有用,例如实现缓存(如 LRU 缓存)、记录操作历史等。
2.2 继承关系与实现接口
LinkedHashMap
的继承关系如下:
plaintext
java.lang.Object
└─ java.util.AbstractMap<K,V>
└─ java.util.HashMap<K,V>
└─ java.util.LinkedHashMap<K,V>
它实现了 Map<K,V>
、Cloneable
和 java.io.Serializable
接口,具备克隆和序列化的能力。
java
import java.util.LinkedHashMap;
import java.util.Map;
public class LinkedHashMapOverview {
public static void main(String[] args) {
// 创建一个 LinkedHashMap 对象
LinkedHashMap<String, Integer> linkedHashMap = new LinkedHashMap<>();
// 可以将其赋值给 Map 接口类型的变量
Map<String, Integer> map = linkedHashMap;
}
}
三、底层数据结构:哈希表与双向链表
3.1 哈希表的作用
LinkedHashMap
基于 HashMap
实现,HashMap
的底层是哈希表(数组 + 链表 + 红黑树)。哈希表通过哈希函数将键映射到数组的某个位置,这个位置称为桶(Bucket)。当多个键映射到同一个桶时,会发生哈希冲突,HashMap
使用链地址法来解决冲突,即每个桶中存储一个链表或红黑树。
3.2 双向链表的作用
LinkedHashMap
额外维护了一个双向链表,链表中的节点不仅包含键值对,还包含指向前一个节点和后一个节点的引用。这个双向链表按照元素的插入顺序或者访问顺序将所有元素连接起来,从而保证了元素的有序性。
3.3 节点结构
LinkedHashMap
中的节点是 Entry
类型,它继承自 HashMap.Node
,并额外添加了 before
和 after
两个引用,用于维护双向链表的顺序。
java
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);
}
}
四、核心属性
LinkedHashMap
有几个重要的核心属性,这些属性控制着 LinkedHashMap
的行为和状态:
java
// 双向链表的头节点
transient LinkedHashMap.Entry<K,V> head;
// 双向链表的尾节点
transient LinkedHashMap.Entry<K,V> tail;
// 访问顺序标志,默认为 false,表示按照插入顺序
final boolean accessOrder;
head
:指向双向链表的头节点,即最早插入的元素。tail
:指向双向链表的尾节点,即最晚插入的元素。accessOrder
:一个布尔值,用于控制双向链表的顺序。如果为false
,则按照元素的插入顺序;如果为true
,则按照元素的访问顺序。
五、构造方法
5.1 无参构造方法
java
public LinkedHashMap() {
super();
accessOrder = false;
}
无参构造方法调用父类 HashMap
的构造方法,并将 accessOrder
设置为 false
,表示按照插入顺序维护元素。
5.2 指定初始容量的构造方法
java
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
该构造方法允许指定 LinkedHashMap
的初始容量,accessOrder
仍然为 false
。
5.3 指定初始容量和负载因子的构造方法
java
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
此构造方法允许同时指定初始容量和负载因子,accessOrder
为 false
。
5.4 指定初始容量、负载因子和访问顺序的构造方法
java
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
该构造方法允许指定初始容量、负载因子和访问顺序,通过 accessOrder
参数可以控制元素是按照插入顺序还是访问顺序排列。
5.5 从其他 Map
创建 LinkedHashMap
的构造方法
java
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super();
accessOrder = false;
putMapEntries(m, false);
}
此构造方法接受一个 Map
对象作为参数,将该 Map
中的所有键值对添加到新创建的 LinkedHashMap
中,accessOrder
为 false
。
java
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
public class LinkedHashMapConstructors {
public static void main(String[] args) {
// 无参构造方法
LinkedHashMap<String, Integer> linkedHashMap1 = new LinkedHashMap<>();
// 指定初始容量的构造方法
LinkedHashMap<String, Integer> linkedHashMap2 = new LinkedHashMap<>(20);
// 指定初始容量和负载因子的构造方法
LinkedHashMap<String, Integer> linkedHashMap3 = new LinkedHashMap<>(15, 0.8f);
// 指定初始容量、负载因子和访问顺序的构造方法
LinkedHashMap<String, Integer> linkedHashMap4 = new LinkedHashMap<>(10, 0.75f, true);
// 从其他 Map 创建 LinkedHashMap 的构造方法
Map<String, Integer> hashMap = new HashMap<>();
hashMap.put("apple", 1);
hashMap.put("banana", 2);
LinkedHashMap<String, Integer> linkedHashMap5 = new LinkedHashMap<>(hashMap);
System.out.println("LinkedHashMap1: " + linkedHashMap1);
System.out.println("LinkedHashMap2: " + linkedHashMap2);
System.out.println("LinkedHashMap3: " + linkedHashMap3);
System.out.println("LinkedHashMap4: " + linkedHashMap4);
System.out.println("LinkedHashMap5: " + linkedHashMap5);
}
}
六、常用操作原理
6.1 插入元素(put 方法)
LinkedHashMap
的 put
方法实际上是调用父类 HashMap
的 put
方法,在插入元素时,会创建一个新的 Entry
节点,并将其添加到哈希表中,同时将该节点添加到双向链表的尾部。
java
// HashMap 的 put 方法
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
// LinkedHashMap 重写的 newNode 方法
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(hash, key, value, e);
linkNodeLast(p);
return p;
}
// 将节点添加到双向链表的尾部
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;
}
}
6.2 获取元素(get 方法)
java
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
afterNodeAccess(e);
return e.value;
}
// 访问节点后调整双向链表的顺序
void afterNodeAccess(Node<K,V> e) { // move node to last
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;
}
}
当 accessOrder
为 true
时,每次访问元素后,会将该元素移动到双向链表的尾部,从而保证链表按照访问顺序排列。
6.3 删除元素(remove 方法)
java
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
// LinkedHashMap 重写的 afterNodeRemoval 方法
void afterNodeRemoval(Node<K,V> e) { // unlink
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;
}
删除元素时,会将该元素从哈希表中移除,同时从双向链表中移除。
6.4 遍历元素
LinkedHashMap
支持多种遍历方式,例如使用 entrySet()
、keySet()
和 values()
方法获取相应的集合,然后使用迭代器或增强 for
循环进行遍历。由于 LinkedHashMap
维护了双向链表,遍历顺序会按照插入顺序或者访问顺序进行。
java
import java.util.LinkedHashMap;
import java.util.Map;
public class LinkedHashMapTraversal {
public static void main(String[] args) {
LinkedHashMap<String, Integer> linkedHashMap = new LinkedHashMap<>();
linkedHashMap.put("apple", 1);
linkedHashMap.put("banana", 2);
linkedHashMap.put("cherry", 3);
// 遍历键值对
for (Map.Entry<String, Integer> entry : linkedHashMap.entrySet()) {
System.out.println(entry.getKey() + ": " + entry.getValue());
}
// 遍历键
for (String key : linkedHashMap.keySet()) {
System.out.println(key);
}
// 遍历值
for (Integer value : linkedHashMap.values()) {
System.out.println(value);
}
}
}
七、LRU 缓存实现示例
LinkedHashMap
可以很方便地实现 LRU(Least Recently Used,最近最少使用)缓存。通过设置 accessOrder
为 true
,并重写 removeEldestEntry
方法,可以在缓存达到最大容量时自动移除最旧的元素。
java
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) {
// 初始容量为 capacity,负载因子为 0.75f,accessOrder 为 true
super(capacity, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
// 当元素数量超过容量时,移除最旧的元素
return size() > capacity;
}
};
this.capacity = capacity;
}
public static void main(String[] args) {
LRUCache<String, Integer> cache = new LRUCache<>(3);
cache.put("apple", 1);
cache.put("banana", 2);
cache.put("cherry", 3);
System.out.println(cache); // 输出: {apple=1, banana=2, cherry=3}
cache.get("apple");
System.out.println(cache); // 输出: {banana=2, cherry=3, apple=1}
cache.put("date", 4);
System.out.println(cache); // 输出: {cherry=3, apple=1, date=4}
}
}
八、性能分析
8.1 时间复杂度
- 插入操作:插入操作的平均时间复杂度为 O(1),因为哈希表可以通过哈希函数快速定位到桶的位置,在没有哈希冲突的情况下,插入操作可以在常数时间内完成。同时,将节点添加到双向链表的尾部也只需要常数时间。
- 查找操作 :查找操作的平均时间复杂度为 O(1),同样可以通过哈希函数快速定位到桶的位置,然后在桶中查找元素。如果
accessOrder
为true
,还需要将访问的节点移动到双向链表的尾部,这也只需要常数时间。 - 删除操作:删除操作的平均时间复杂度为 O(1),通过哈希函数定位到桶的位置,然后在桶中删除元素,并从双向链表中移除该节点。
8.2 空间复杂度
LinkedHashMap
的空间复杂度为 O(n),主要用于存储哈希表和双向链表中的节点。
九、注意事项
9.1 线程安全问题
LinkedHashMap
不是线程安全的。如果在多线程环境下需要使用线程安全的 Map
,可以考虑使用 Collections.synchronizedMap()
方法将 LinkedHashMap
包装成线程安全的 Map
。
java
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
public class LinkedHashMapThreadSafety {
public static void main(String[] args) {
LinkedHashMap<String, Integer> linkedHashMap = new LinkedHashMap<>();
Map<String, Integer> synchronizedMap = Collections.synchronizedMap(linkedHashMap);
}
}
9.2 性能考虑
虽然 LinkedHashMap
的插入、查找和删除操作的平均时间复杂度与 HashMap
相同,但由于需要维护双向链表,会有一定的额外开销。因此,在不需要维护元素顺序的场景下,使用 HashMap
可能会有更好的性能。
十、总结
LinkedHashMap
是 Java 集合框架中一个非常实用的类,它结合了 HashMap
的快速查找和插入功能,以及双向链表的有序性。通过 accessOrder
参数,可以控制元素是按照插入顺序还是访问顺序排列。在实现 LRU 缓存等场景中,LinkedHashMap
表现出了强大的优势。但在使用时,需要注意线程安全问题和性能考虑。深入理解 LinkedHashMap
的原理和使用方法,有助于我们在实际开发中更好地利用它来解决各种问题。