一、引言
在 Java 的集合框架中,LinkedHashSet
是一个独特且实用的类。它结合了 HashSet
的元素唯一性和链表的有序性,既能够确保存储的元素不重复,又能按照元素插入的顺序或者访问的顺序来维护元素。本文将深入探讨 LinkedHashSet
的原理,包括其底层数据结构、核心属性、构造方法、常用操作的实现细节,还会结合丰富的代码示例进行详细说明。
二、LinkedHashSet 概述
2.1 定义与用途
LinkedHashSet
是 Java 集合框架中 Set
接口的一个具体实现类,位于 java.util
包下。它继承自 HashSet
,并额外维护了一个双向链表,用于记录元素的插入顺序或者访问顺序。这使得 LinkedHashSet
非常适合那些需要保持元素插入顺序,同时又要保证元素唯一性的场景,例如缓存、历史记录管理等。
2.2 继承关系与实现接口
LinkedHashSet
的继承关系如下:
plaintext
java.lang.Object
└─ java.util.AbstractCollection<E>
└─ java.util.AbstractSet<E>
└─ java.util.HashSet<E>
└─ java.util.LinkedHashSet<E>
它实现了 Set
、Cloneable
和 java.io.Serializable
接口,具备集合的基本操作,支持克隆和序列化。
java
import java.util.LinkedHashSet;
import java.util.Set;
public class LinkedHashSetOverview {
public static void main(String[] args) {
// 创建一个 LinkedHashSet 对象
LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>();
// 可以将其赋值给 Set 接口类型的变量
Set<String> set = linkedHashSet;
}
}
三、底层数据结构:哈希表与双向链表
3.1 哈希表的作用
LinkedHashSet
基于 HashSet
实现,而 HashSet
底层是基于哈希表(实际上是 HashMap
)。哈希表通过哈希函数将元素映射到一个固定大小的数组中的某个位置,这个位置称为桶(Bucket)。当多个元素映射到同一个桶时,就会发生哈希冲突。LinkedHashSet
使用链地址法来解决哈希冲突,即每个桶中存储一个链表,链表中的节点存储元素。
3.2 双向链表的作用
LinkedHashSet
额外维护了一个双向链表,链表中的节点不仅包含元素本身,还包含指向前一个节点和后一个节点的引用。这个双向链表按照元素的插入顺序或者访问顺序将所有元素连接起来,从而保证了元素的有序性。
3.3 节点结构
LinkedHashSet
中的节点是 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);
}
}
四、核心属性
LinkedHashSet
本身没有定义太多的核心属性,主要依赖于 HashSet
和 LinkedHashMap
的属性。其中,LinkedHashMap
是 LinkedHashSet
底层实际使用的数据结构,它有以下几个重要属性:
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 LinkedHashSet() {
super(16, .75f, true);
}
无参构造方法调用父类 HashSet
的构造方法,创建一个初始容量为 16,负载因子为 0.75 的 LinkedHashSet
,并将 accessOrder
设置为 true
,表示按照插入顺序维护元素。
5.2 指定初始容量的构造方法
java
public LinkedHashSet(int initialCapacity) {
super(initialCapacity, .75f, true);
}
该构造方法允许指定 LinkedHashSet
的初始容量,负载因子默认为 0.75,同样按照插入顺序维护元素。
5.3 指定初始容量和负载因子的构造方法
java
public LinkedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor, true);
}
此构造方法允许同时指定初始容量和负载因子,accessOrder
仍然为 true
。
5.4 从其他集合创建 LinkedHashSet 的构造方法
java
public LinkedHashSet(Collection<? extends E> c) {
super(Math.max(2*c.size(), 11), .75f, true);
addAll(c);
}
该构造方法接受一个集合作为参数,将集合中的元素添加到新创建的 LinkedHashSet
中。初始容量根据传入集合的大小进行调整。
java
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Set;
public class LinkedHashSetConstructors {
public static void main(String[] args) {
// 无参构造方法
LinkedHashSet<String> linkedHashSet1 = new LinkedHashSet<>();
// 指定初始容量的构造方法
LinkedHashSet<String> linkedHashSet2 = new LinkedHashSet<>(20);
// 指定初始容量和负载因子的构造方法
LinkedHashSet<String> linkedHashSet3 = new LinkedHashSet<>(15, 0.8f);
// 从其他集合创建 LinkedHashSet 的构造方法
Collection<String> collection = new ArrayList<>();
collection.add("apple");
collection.add("banana");
LinkedHashSet<String> linkedHashSet4 = new LinkedHashSet<>(collection);
System.out.println(linkedHashSet4);
}
}
六、常用操作原理
6.1 添加元素
java
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
add
方法实际上是调用 LinkedHashMap
的 put
方法将元素作为键,PRESENT
作为值存储到 LinkedHashMap
中。在插入元素时,LinkedHashMap
会先计算元素的哈希值,找到对应的桶,然后遍历桶中的链表,检查元素是否已经存在。如果元素不存在,则创建一个新的 LinkedHashMap.Entry
节点,并将其插入到哈希表的桶中,同时将该节点添加到双向链表的尾部。
6.2 检查元素是否存在
java
public boolean contains(Object o) {
return map.containsKey(o);
}
contains
方法调用 LinkedHashMap
的 containsKey
方法来检查指定元素是否存在于 LinkedHashSet
中。它会根据元素的哈希值找到对应的桶,然后遍历桶中的链表进行查找。
6.3 删除元素
java
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
remove
方法调用 LinkedHashMap
的 remove
方法来删除指定元素。在删除元素时,LinkedHashMap
会先找到该元素对应的节点,然后将其从哈希表的桶中移除,同时将该节点从双向链表中移除。
6.4 获取元素数量
java
public int size() {
return map.size();
}
size
方法调用 LinkedHashMap
的 size
方法返回 LinkedHashSet
中元素的数量。
java
import java.util.LinkedHashSet;
public class LinkedHashSetOperations {
public static void main(String[] args) {
LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>();
// 添加元素
linkedHashSet.add("apple");
linkedHashSet.add("banana");
linkedHashSet.add("cherry");
// 检查元素是否存在
System.out.println("Contains apple: " + linkedHashSet.contains("apple"));
// 删除元素
linkedHashSet.remove("banana");
System.out.println("After removing banana: " + linkedHashSet);
// 获取元素数量
System.out.println("Size: " + linkedHashSet.size());
}
}
七、迭代器原理
LinkedHashSet
的迭代器是基于双向链表实现的,因此迭代器会按照元素的插入顺序或者访问顺序依次访问元素。迭代器的实现相对简单,只需要从双向链表的头节点开始,依次访问每个节点即可。
java
import java.util.Iterator;
import java.util.LinkedHashSet;
public class LinkedHashSetIterator {
public static void main(String[] args) {
LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>();
linkedHashSet.add("apple");
linkedHashSet.add("banana");
linkedHashSet.add("cherry");
Iterator<String> iterator = linkedHashSet.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}
八、性能分析
8.1 时间复杂度
- 插入操作:插入操作的平均时间复杂度为 O(1),因为哈希表可以通过哈希函数快速定位到桶的位置,在没有哈希冲突的情况下,插入操作可以在常数时间内完成。但在极端情况下,当所有元素都映射到同一个桶时,插入操作的时间复杂度会退化为 O(n)。
- 查找操作:查找操作的平均时间复杂度为 O(1),同样可以通过哈希函数快速定位到桶的位置,然后在桶中查找元素。
- 删除操作:删除操作的平均时间复杂度为 O(1),通过哈希函数定位到桶的位置,然后在桶中删除元素,并从双向链表中移除该节点。
8.2 空间复杂度
LinkedHashSet
的空间复杂度为 O(n),主要用于存储哈希表和双向链表中的节点。
九、注意事项
9.1 元素的哈希码和相等性
LinkedHashSet
判断元素是否重复是基于元素的 hashCode()
和 equals()
方法。因此,存储在 LinkedHashSet
中的元素必须正确重写这两个方法,否则可能会导致元素重复存储或查找失败。
9.2 线程安全问题
LinkedHashSet
不是线程安全的。如果在多线程环境下需要使用线程安全的集合,可以考虑使用 Collections.synchronizedSet()
方法将 LinkedHashSet
包装成线程安全的集合。
java
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
public class LinkedHashSetThreadSafety {
public static void main(String[] args) {
LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>();
Set<String> synchronizedSet = Collections.synchronizedSet(linkedHashSet);
}
}
十、总结
LinkedHashSet
是 Java 集合框架中一个非常实用的类,它结合了 HashSet
的元素唯一性和链表的有序性。通过哈希表和双向链表的组合,LinkedHashSet
能够高效地进行元素的插入、查找和删除操作,同时保持元素的插入顺序或访问顺序。在使用 LinkedHashSet
时,需要注意元素的哈希码和相等性的重写,以及线程安全问题。深入理解 LinkedHashSet
的原理和性能特点,有助于我们在实际开发中合理地使用它,提高程序的性能和可靠性。