Set 选型对比:从选型角度出发
先问自己三个问题
拿到一个需求,不要急着写代码。先回答这三个问题:
问题 1:需要排序吗?
└─ 需要 → TreeSet / LinkedHashSet
└─ 不需要 → 问题 2
问题 2:需要保持插入顺序吗?
└─ 需要 → LinkedHashSet
└─ 不需要 → HashSet
问题 3:需要并发安全吗?
└─ 需要 → ConcurrentSkipListSet / CopyOnWriteArraySet
└─ 不需要 → 以上对应版本四种 Set 一张表看懂
| 实现类 | 底层 | 有序性 | add/contains | null | 线程安全 |
|---|---|---|---|---|---|
| HashSet | HashMap | 无序 | O(1) | ✅ 1个 | ❌ |
| LinkedHashSet | LinkedHashMap | 插入顺序 | O(1) | ✅ 1个 | ❌ |
| TreeSet | TreeMap | 排序 | 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,并发场景看读写比。
