LinkedHashSet 是 Java 中的一个集合类,它继承自 HashSet 并实现了 Set 接口。与 HashSet 一样, LinkedHashSet 不允许重复元素,但它维护了元素插入的顺序,即元素迭代的顺序与它们插入的顺序相同。 LinkedHashSet 在内部使用链表来维护元素的插入顺序,同时使用哈希表来快速定位元素,这使得它在保持快速查找性能的同时,还能够按插入顺序遍历元素。由于其基于哈希表和链表的实现, LinkedHashSet 在进行元素插入和删除操作时具有较高的性能,但在随机访问操作上的性能不如基于动态数组的 ArrayList。 LinkedHashSet 是非线程安全的,适用于需要保持插入顺序的场景,如需要有序去重或有序集合操作。
1、 LinkedHashSet
LinkedHashSet 是 Java 集合框架中的一个成员,它结合了 HashSet 的快速查找特性和 LinkedList 的插入顺序保持功能。以下是 LinkedHashSet 的设计:
设计思考:
- 需求场景:
在很多应用场景中,需要快速地插入、删除和查找元素,同时也需要保持元素的插入顺序。
例如,在处理用户会话、缓存实现、任务调度等场景时,保持元素的添加顺序是非常重要的。
- 现有技术局限性:
HashSet 提供了常数时间的添加、删除和查找性能,但它不保持元素的插入顺序。
TreeSet 保持了元素的排序顺序,但不是插入顺序,且它的性能不如 HashSet。
ArrayList 和 LinkedList 保持了插入顺序,但它们的查找性能为线性时间复杂度。
技术融合:
为了结合 HashSet 的快速查找能力和 LinkedList 的插入顺序保持能力, LinkedHashSet 应运而生。
设计理念:
LinkedHashSet 底层使用 HashMap 来存储元素,保证了快速的查找性能。
同时,它在每个 HashMap 的条目上使用一个双向链表来维护元素的插入顺序。
实现方式:
LinkedHashSet 继承自 HashSet,但重写了 add、 iterator 等方法,以维护插入顺序。
它在内部维护了与 HashMap 条目关联的双向链表的节点,这些节点链接了具有相同哈希值但插入顺序不同的元素。
2、 数据结构
图片
图说明:
- LinkedHashSet:
表示 LinkedHashSet 类的实例,它继承自 HashSet 并维护元素的插入顺序。
- HashMap:
LinkedHashSet 的实现基于 HashMap,用来存储集合中的元素。
数组 (Buckets) :
HashMap 使用一个数组来存储桶(Buckets),桶是用于存储 Entry 对象的容器。
哈希桶:
每个桶内部使用链表来解决哈希冲突。
链表 Entry:
每个桶包含多个 Entry 对象,它们通过链表连接。
红黑树 Entry:
当链表长度超过阈值时,链表可能会被转换成红黑树以提高搜索效率。
链表 节点1 和 链表 节点2:
表示链表中的节点,每个节点存储着集合中的一个元素,并指向前一个和后一个节点,形成双向链表。
元素:
存储在 LinkedHashSet 中的最终数据。
3、 执行流程
图片
图说明:
- 创建 LinkedHashSet 实例:
初始化 LinkedHashSet 对象。
- 添加元素:
将元素添加到 LinkedHashSet。
计算元素的hashCode:
调用元素的 hashCode() 方法计算其哈希码。
确定数组索引位置:
根据哈希码和数组长度确定数组索引位置。
找到对应的哈希桶:
定位到数组中对应的哈希桶。
检查哈希桶中的链表/红黑树:
检查哈希桶中是否已有链表或红黑树结构。
处理哈希冲突:
如果桶中已有元素,处理哈希冲突。
元素添加至链表/红黑树:
将新元素添加至对应索引的链表或红黑树中。
删除元素:
从 LinkedHashSet 删除元素。
重新计算元素的hashCode:
调用元素的 hashCode() 方法计算其哈希码。
确定删除元素的数组索引位置:
根据哈希码和数组长度确定数组索引位置。
找到删除元素的哈希桶:
定位到数组中对应的哈希桶。
从链表/红黑树中删除元素:
从对应索引的链表或红黑树中删除元素。
遍历 LinkedHashSet:
遍历 LinkedHashSet 中的所有元素。
获取数组:
获取 LinkedHashSet 内部的数组。
遍历每个桶:
遍历数组的每个桶。
遍历链表/红黑树:
遍历桶内的链表或红黑树中的所有元素。
读取元素:
读取链表或红黑树中的元素。
4、优点:
- 快速查找:
继承自 HashSet,具有快速的查找、添加和删除操作。
- 保持插入顺序:
通过内部维护的双向链表,保持了元素的插入顺序。
空间和时间效率:
相对于 TreeSet, LinkedHashSet 在大多数情况下具有更好的性能。
5、缺点:
- 内存占用:
相比于 HashSet, LinkedHashSet 需要额外的内存来维护双向链表。
- 复杂性:
相比于简单的 HashSet, LinkedHashSet 的实现和使用复杂度稍高。
6、使用场景:
- 需要快速查找和保持插入顺序的场景,如 LRU 缓存、任务调度、用户会话管理等。
7、类设计
图片
8、应用案例
LinkedHashSet 通常用于需要保持元素插入顺序的场景。这是一个用户会话管理器,用于跟踪用户的登录状态和最后活跃时间:
import java.util.LinkedHashSet;
import java.util.Set;
// 用户类,用于表示系统中的用户
class User {
private String id;
private String username;
private long lastActiveTime;
public User(String id, String username, long lastActiveTime) {
this.id = id;
this.username = username;
this.lastActiveTime = lastActiveTime;
}
// 省略 getter 和 setter 方法
@Override
public String toString() {
return "User{" +
"id='" + id + ''' +
", username='" + username + ''' +
", lastActiveTime=" + lastActiveTime +
'}';
}
}
// 用户会话管理器类
class UserSessionManager {
private Set<User> activeUsers;
public UserSessionManager() {
activeUsers = new LinkedHashSet<>();
}
// 添加或更新用户会话
public void addUser(User user) {
activeUsers.add(user);
}
// 获取所有活跃用户
public Set<User> getActiveUsers() {
return activeUsers;
}
// 移除用户会话
public void removeUser(String userId) {
// 遍历 LinkedHashSet 以找到并移除指定用户
for (User user : activeUsers) {
if (user.getId().equals(userId)) {
activeUsers.remove(user);
break;
}
}
}
}
public class Main {
public static void main(String[] args) {
UserSessionManager sessionManager = new UserSessionManager();
// 模拟用户登录
sessionManager.addUser(new User("1", "Alice", System.currentTimeMillis()));
sessionManager.addUser(new User("2", "Bob", System.currentTimeMillis()));
// 获取并打印所有活跃用户
Set<User> activeUsers = sessionManager.getActiveUsers();
for (User user : activeUsers) {
System.out.println("Active User: " + user);
}
// 模拟用户注销
sessionManager.removeUser("1");
// 再次获取并打印所有活跃用户
activeUsers = sessionManager.getActiveUsers();
for (User user : activeUsers) {
System.out.println("Active User: " + user);
}
}
}