图解LinkedHashSet数据结构设计与应用案例

298 阅读5分钟

image.png

LinkedHashSet 是 Java 中的一个集合类,它继承自 HashSet 并实现了 Set 接口。与 HashSet 一样,LinkedHashSet 不允许重复元素,但它维护了元素插入的顺序,即元素迭代的顺序与它们插入的顺序相同。LinkedHashSet 在内部使用链表来维护元素的插入顺序,同时使用哈希表来快速定位元素,这使得它在保持快速查找性能的同时,还能够按插入顺序遍历元素。由于其基于哈希表和链表的实现,LinkedHashSet 在进行元素插入和删除操作时具有较高的性能,但在随机访问操作上的性能不如基于动态数组的 ArrayListLinkedHashSet 是非线程安全的,适用于需要保持插入顺序的场景,如需要有序去重或有序集合操作。

1、 LinkedHashSet

LinkedHashSet 是 Java 集合框架中的一个成员,它结合了 HashSet 的快速查找特性和 LinkedList 的插入顺序保持功能。以下是 LinkedHashSet 的设计:

设计思考:

  1. 需求场景
    • 在很多应用场景中,需要快速地插入、删除和查找元素,同时也需要保持元素的插入顺序。
    • 例如,在处理用户会话、缓存实现、任务调度等场景时,保持元素的添加顺序是非常重要的。
  2. 现有技术局限性
    • HashSet 提供了常数时间的添加、删除和查找性能,但它不保持元素的插入顺序。
    • TreeSet 保持了元素的排序顺序,但不是插入顺序,且它的性能不如 HashSet
    • ArrayList 和 LinkedList 保持了插入顺序,但它们的查找性能为线性时间复杂度。
  3. 技术融合
    • 为了结合 HashSet 的快速查找能力和 LinkedList 的插入顺序保持能力,LinkedHashSet 应运而生。
  4. 设计理念
    • LinkedHashSet 底层使用 HashMap 来存储元素,保证了快速的查找性能。
    • 同时,它在每个 HashMap 的条目上使用一个双向链表来维护元素的插入顺序。
  5. 实现方式
    • LinkedHashSet 继承自 HashSet,但重写了 additerator 等方法,以维护插入顺序。
    • 它在内部维护了与 HashMap 条目关联的双向链表的节点,这些节点链接了具有相同哈希值但插入顺序不同的元素。

2、 数据结构

image.png

图说明:
  • LinkedHashSet:
    • 表示 LinkedHashSet 类的实例,它继承自 HashSet 并维护元素的插入顺序。
  • HashMap:
    • LinkedHashSet 的实现基于 HashMap,用来存储集合中的元素。
  • 数组 (Buckets) :
    • HashMap 使用一个数组来存储桶(Buckets),桶是用于存储 Entry 对象的容器。
  • 哈希桶:
    • 每个桶内部使用链表来解决哈希冲突。
  • 链表 Entry:
    • 每个桶包含多个 Entry 对象,它们通过链表连接。
  • 红黑树 Entry:
    • 当链表长度超过阈值时,链表可能会被转换成红黑树以提高搜索效率。
  • 链表 节点1链表 节点2:
    • 表示链表中的节点,每个节点存储着集合中的一个元素,并指向前一个和后一个节点,形成双向链表。
  • 元素:
    • 存储在 LinkedHashSet 中的最终数据。

3、 执行流程

image.png

图说明:
  • 创建 LinkedHashSet 实例:
    • 初始化 LinkedHashSet 对象。
  • 添加元素:
    • 将元素添加到 LinkedHashSet
  • 计算元素的hashCode:
    • 调用元素的 hashCode() 方法计算其哈希码。
  • 确定数组索引位置:
    • 根据哈希码和数组长度确定数组索引位置。
  • 找到对应的哈希桶:
    • 定位到数组中对应的哈希桶。
  • 检查哈希桶中的链表/红黑树:
    • 检查哈希桶中是否已有链表或红黑树结构。
  • 处理哈希冲突:
    • 如果桶中已有元素,处理哈希冲突。
  • 元素添加至链表/红黑树:
    • 将新元素添加至对应索引的链表或红黑树中。
  • 删除元素:
    • 从 LinkedHashSet 删除元素。
  • 重新计算元素的hashCode:
    • 调用元素的 hashCode() 方法计算其哈希码。
  • 确定删除元素的数组索引位置:
    • 根据哈希码和数组长度确定数组索引位置。
  • 找到删除元素的哈希桶:
    • 定位到数组中对应的哈希桶。
  • 从链表/红黑树中删除元素:
    • 从对应索引的链表或红黑树中删除元素。
  • 遍历 LinkedHashSet:
    • 遍历 LinkedHashSet 中的所有元素。
  • 获取数组:
    • 获取 LinkedHashSet 内部的数组。
  • 遍历每个桶:
    • 遍历数组的每个桶。
  • 遍历链表/红黑树:
    • 遍历桶内的链表或红黑树中的所有元素。
  • 读取元素:
    • 读取链表或红黑树中的元素。

4、优点:

  1. 快速查找
    • 继承自 HashSet,具有快速的查找、添加和删除操作。
  2. 保持插入顺序
    • 通过内部维护的双向链表,保持了元素的插入顺序。
  3. 空间和时间效率
    • 相对于 TreeSetLinkedHashSet 在大多数情况下具有更好的性能。

5、缺点:

  1. 内存占用
    • 相比于 HashSetLinkedHashSet 需要额外的内存来维护双向链表。
  2. 复杂性
    • 相比于简单的 HashSetLinkedHashSet 的实现和使用复杂度稍高。

6、使用场景:

  • 需要快速查找和保持插入顺序的场景,如 LRU 缓存、任务调度、用户会话管理等。

7、类设计

image.png

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