1. 概述
Set 是 Java 中常用的集合类型之一。今天我们来探讨如何计算两个 Set 之间的差集。
2. 问题解析
在深入实现前,先明确问题场景。通过一个例子快速理解需求:
假设有两个 Set 对象:
set1: {"Kotlin", "Java", "Rust", "Python", "C++"}
set2: {"Kotlin", "Java", "Rust", "Ruby", "C#"}
"计算两个 Set 的差集"通常有两种变体:
- 非对称差集:找出 set1 中存在但 set2 中不存在的元素,预期结果为
{"Python", "C++"}
- 对称差集:找出两个集合中互不相同的元素,预期结果为
{"Python", "C++", "Ruby", "C#"}
本文将分别解决这两种场景。先从非对称差集开始,再探讨对称差集的实现方案。
3. 非对称差集
3.1 使用标准 removeAll 方法
Set 类提供了 removeAll
方法(继承自 Collection 接口)。该方法接受一个 Collection 参数,从当前 Set 中移除所有包含在参数集合中的元素。因此调用 set1.removeAll(set2)
后,set1 中剩余元素即为差集。
通过单元测试演示:
Set<String> set1 = Stream.of("Kotlin", "Java", "Rust", "Python", "C++").collect(Collectors.toSet());
Set<String> set2 = Stream.of("Kotlin", "Java", "Rust", "Ruby", "C#").collect(Collectors.toSet());
Set<String> expectedOnlyInSet1 = Set.of("Python", "C++");
set1.removeAll(set2);
assertThat(set1).isEqualTo(expectedOnlyInSet1);
此方法简单直接,但有个明显缺点:会修改原始 set1。因此:
- 若后续仍需原始 set1,需提前备份
- 若 set1 是不可变集合,必须先创建可变副本
3.2 使用 Stream.filter 方法
Java 8 引入的 Stream API 可通过 filter
方法过滤元素。此方案不修改原始集合,特别适合不可变 Set:
Set<String> immutableSet1 = Set.of("Kotlin", "Java", "Rust", "Python", "C++");
Set<String> immutableSet2 = Set.of("Kotlin", "Java", "Rust", "Ruby", "C#");
Set<String> expectedOnlyInSet1 = Set.of("Python", "C++");
Set<String> actualOnlyInSet1 = immutableSet1.stream()
.filter(e -> !immutableSet2.contains(e))
.collect(Collectors.toSet());
assertThat(actualOnlyInSet1).isEqualTo(expectedOnlyInSet1);
核心逻辑在 filter(e -> !immutableSet2.contains(e))
,仅保留 set1 中存在而 set2 中不存在的元素。测试通过且原始集合未被修改。
3.3 使用 Guava 库
Guava 是流行的 Java 工具库,提供了便捷的集合操作方法。首先添加 Maven 依赖:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.1.3-jre</version>
</dependency>
使用 Sets.difference
方法获取差集:
Set<String> actualOnlyInSet1 = Sets.difference(immutableSet1, immutableSet2);
assertThat(actualOnlyInSet1).isEqualTo(expectedOnlyInSet1);
需注意:该方法返回的是不可变 Set 视图,意味着:
- 返回结果不可修改
- 若原始集合是可变的,其变更会反映到结果视图中
3.4 使用 Apache Commons 库
Apache Commons Collections4 提供了丰富的集合工具方法。添加 Maven 依赖:
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
使用 CollectionUtils.removeAll
方法:
Set<String> actualOnlyInSet1 = new HashSet<>(
CollectionUtils.removeAll(immutableSet1, immutableSet2)
);
assertThat(actualOnlyInSet1).isEqualTo(expectedOnlyInSet1);
关键区别:此方法返回 Collection 类型而非 Set。若需要 Set 类型,需手动转换(如示例中包装为 HashSet)。
4. 对称差集
现在探讨另一种场景:计算两个集合的对称差集。预期结果:
Set<String> expectedDiff = Set.of("Python", "C++", "Ruby", "C#");
4.1 使用 HashMap 方案
核心思路:创建 Map<T, Integer>
统计元素出现次数
- 遍历两个集合,将元素作为 key 存入 map
- 若 key 已存在(即两集合共有元素),将其值设为特殊标记(如 Integer.MAX_VALUE)
- 否则存入新条目(key, 1)
- 最后提取值为 1 的 key 即为对称差集
实现代码:
public static <T> Set<T> findSymmetricDiff(Set<T> set1, Set<T> set2) {
Map<T, Integer> map = new HashMap<>();
set1.forEach(e -> putKey(map, e));
set2.forEach(e -> putKey(map, e));
return map.entrySet().stream()
.filter(e -> e.getValue() == 1)
.map(Map.Entry::getKey)
.collect(Collectors.toSet());
}
private static <T> void putKey(Map<T, Integer> map, T key) {
if (map.containsKey(key)) {
map.replace(key, Integer.MAX_VALUE);
} else {
map.put(key, 1);
}
}
测试验证:
Set<String> actualDiff = SetDiff.findSymmetricDiff(immutableSet1, immutableSet2);
assertThat(actualDiff).isEqualTo(expectedDiff);
4.2 使用 Apache Commons 库
Apache Commons 提供了更直接的解决方案:
Set<String> actualDiff = SetUtils.disjunction(immutableSet1, immutableSet2);
assertThat(actualDiff).isEqualTo(expectedDiff);
优势:直接返回 Set 类型,无需手动转换。相比 CollectionUtils.removeAll
更符合场景需求。
5. 总结
本文通过示例演示了计算 Set 差集的多种方案,涵盖两种核心场景:
- 非对称差集:使用
removeAll
(需注意修改原集合)、Stream.filter
、Guava 的Sets.difference
或 Apache Commons 的CollectionUtils.removeAll
- 对称差集:通过 HashMap 自定义实现或直接使用 Apache Commons 的
SetUtils.disjunction
实际开发中,根据需求选择合适方案:
- 需要高性能且不依赖第三方库时,优先使用 Stream API
- 追求代码简洁性时,Guava/Apache Commons 是更优选择
- 处理不可变集合时,避免使用会修改原集合的方法
本文所有示例代码可在 GitHub 获取。