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> 统计元素出现次数

  1. 遍历两个集合,将元素作为 key 存入 map
  2. 若 key 已存在(即两集合共有元素),将其值设为特殊标记(如 Integer.MAX_VALUE)
  3. 否则存入新条目(key, 1)
  4. 最后提取值为 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 获取。


原始标题:Find the Difference Between Two Sets