Skip to content

Set 选型对比:从选型角度出发

先问自己三个问题

拿到一个需求,不要急着写代码。先回答这三个问题:

问题 1:需要排序吗?
  └─ 需要 → TreeSet / LinkedHashSet
  └─ 不需要 → 问题 2

问题 2:需要保持插入顺序吗?
  └─ 需要 → LinkedHashSet
  └─ 不需要 → HashSet

问题 3:需要并发安全吗?
  └─ 需要 → ConcurrentSkipListSet / CopyOnWriteArraySet
  └─ 不需要 → 以上对应版本

四种 Set 一张表看懂

实现类底层有序性add/containsnull线程安全
HashSetHashMap无序O(1)✅ 1个
LinkedHashSetLinkedHashMap插入顺序O(1)✅ 1个
TreeSetTreeMap排序O(log n)
CopyOnWriteArraySet数组无序O(n)✅ 多个
ConcurrentSkipListSet跳表排序O(log n)

HashSet:通用去重的首选

适用场景:不需要顺序、不需要排序,纯粹去重。

java
// 场景 1:用户 ID 去重
Set<String> userIds = new HashSet<>();
userIds.add(userId);

// 场景 2:关键词过滤
Set<String> bannedWords = new HashSet<>(Arrays.asList("spam", "ad"));
if (bannedWords.contains(inputWord)) {
    return "禁止词";
}

// 场景 3:布隆过滤器的前置(简单版)
Set<Long> seen = new HashSet<>();
if (seen.contains(hashedItem)) {
    return "可能重复";
}

LinkedHashSet:去重 + 保持顺序

适用场景:去重且需要保持原始顺序。

java
// 场景 1:去除重复但保持首次出现顺序
List<String> urls = Arrays.asList(
    "a.com", "b.com", "a.com", "c.com", "b.com"
);
Set<String> unique = new LinkedHashSet<>(urls);
// [a.com, b.com, c.com]

// 场景 2:用户访问历史
LinkedHashSet<String> recent = new LinkedHashSet<>();
recent.add("profile");
recent.add("settings");
recent.add("profile"); // 再次访问
// recent 顺序:settings, profile(profile 被移到最后)

TreeSet:需要排序的能力

适用场景:需要范围查询、最值查找、排名等有序操作。

java
// 场景 1:排行榜
TreeSet<Player> leaderboard = new TreeSet<>(
    Comparator.comparingInt(Player::getScore).reversed()
);
leaderboard.add(new Player("张三", 1500));
leaderboard.add(new Player("李四", 2000));
System.out.println(leaderboard.first()); // 李四(最高分)

// 场景 2:价格区间过滤
TreeSet<Double> prices = new TreeSet<>();
prices.addAll(List.of(99.9, 199.0, 49.9, 299.0, 79.9));
// 100-200 元区间
SortedSet<Double> range = prices.subSet(100.0, 200.0);
System.out.println(range); // [199.0]

// 场景 3:定时任务调度(按执行时间排序)
TreeSet<LocalDateTime> schedule = new TreeSet<>();
schedule.add(LocalDateTime.now().plusMinutes(5));
schedule.add(LocalDateTime.now().plusMinutes(2));
schedule.add(LocalDateTime.now().plusMinutes(10));
System.out.println(schedule.first()); // 最近要执行的任务

并发 Set:多线程环境

类型适用场景特点
Collections.synchronizedSet(new HashSet<>())偶尔并发全局锁,性能一般
CopyOnWriteArraySet读多写少读不锁,写时复制全量
ConcurrentSkipListSet写多读多无锁,O(log n)
java
// 读多写少 → CopyOnWriteArraySet
Set<String> readHeavy = new CopyOnWriteArraySet<>();
// 每次写入都复制全量数组,写操作很慢
// 但读取完全无锁,极快

// 写多读多 → ConcurrentSkipListSet
Set<String> writeHeavy = new ConcurrentSkipListSet<>();
// 无锁 CAS 操作,适合高并发场景

// 不确定 → 先用 synchronizedSet 保守
Set<String> safe = Collections.synchronizedSet(new HashSet<>());

不要只盯着 Set

有些场景,用 Map 或 List 更合适:

java
// ❌ 用 Set 统计不重复用户数(但需要更多信息)
Set<String> uniqueUsers = new HashSet<>();
uniqueUsers.add(userId);
// 结果:只能知道「有多少个」,无法知道「具体是谁」

// ✅ 用 Map 统计(同时保留更多信息)
Map<String, User> users = new HashMap<>();
users.put(userId, userObject);
// 可以根据 userId 快速查到用户信息

// ❌ 用 Set 做频繁的增删改
TreeSet<Integer> set = new TreeSet<>();
for (int i = 0; i < 100000; i++) {
    set.add(i); // O(log n),频繁再平衡
}
// TreeSet 的每次增删都可能触发树的再平衡,开销大

// ✅ 用 List 做批量操作后再去重
List<Integer> data = generateData();
Set<Integer> unique = new HashSet<>(data); // 批量构造,更快

选型决策树

需要 Set 吗?

├── 只需要「有没有」→ HashSet

├── 需要「顺序」但不需排序
│   └── LinkedHashSet

├── 需要「排序」或「范围查询」
│   ├── 单线程 → TreeSet
│   └── 多线程 → ConcurrentSkipListSet

└── 需要「并发安全」
    ├── 读多写少 → CopyOnWriteArraySet
    └── 写多读多 → ConcurrentSkipListSet
    └── 保守选择 → synchronizedSet

常见误区

误区 1:所有场景都用 HashSet

HashSet 不是万能的。如果需要有序,HashSet 的遍历顺序会让你困惑。

误区 2:TreeSet 比 HashSet 更「高级」

TreeSet 的 O(log n) 比 HashSet 的 O(1) 慢 5-10 倍。只有需要 TreeSet 的独有功能时才用它。

误区 3:并发场景用 synchronizedSet 永远安全

synchronizedSet 在高并发下性能很差。如果读多写少,CopyOnWriteArraySet 更好;如果写多读多,ConcurrentSkipListSet 更好。

误区 4:Set 能替代 List 做所有事

Set 不支持按索引访问,不能有重复元素。如果你需要「可以有重复」或「按位置访问」,用 List。


总结

需求推荐
通用去重,不需要顺序HashSet
去重 + 保持插入顺序LinkedHashSet
需要排序/范围查询TreeSet
读多写少并发CopyOnWriteArraySet
写多读多并发ConcurrentSkipListSet
不确定,先保守synchronizedSet

选型口诀:无序去重用 HashSet,保持顺序用 Linked,需要排序用 TreeSet,并发场景看读写比。


相关链接

基于 VitePress 构建