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);
}
}
}