1. 概述
在实际开发中,我们经常需要比较两个同类型对象集合的差异。比如,有一个参加考试的学生名单,还有一个通过考试的学生名单,两者之间的差集就是未通过考试的学生。
Java 的 List
接口本身没有直接提供“求差集”的方法,但我们可以借助一些 API 或第三方工具类来实现。本文将带你用几种不同的方式实现这一需求:包括原生 Java(传统方式和 Stream)、以及 Guava 和 Apache Commons Collections 等常用库。
✅ 目标明确:找出 list1 - list2
或 list2 - list1
的元素
⚠️ 注意:本文面向有经验的开发者,基础语法不再赘述
2. 测试数据准备
我们先定义两个测试用的列表,后续所有示例都基于它们:
public class FindDifferencesBetweenListsUnitTest {
private static final List<String> listOne = Arrays.asList("Jack", "Tom", "Sam", "John", "James", "Jack");
private static final List<String> listTwo = Arrays.asList("Jack", "Daniel", "Sam", "Alan", "James", "George");
}
listOne
:原始名单(含重复项 "Jack")listTwo
:对比名单- 目标:找出
listOne
有但listTwo
没有的人,反之亦然
3. 使用 Java 原生 List API
最简单粗暴的方式是:复制一个列表,然后删除另一个列表中包含的所有元素。
利用 List.removeAll(Collection)
方法即可:
List<String> differences = new ArrayList<>(listOne);
differences.removeAll(listTwo);
assertEquals(2, differences.size());
assertThat(differences).containsExactly("Tom", "John");
✅ 输出结果:["Tom", "John"]
—— 这两人只在 listOne
出现
⚠️ 注意:removeAll
是破坏性操作,所以必须先复制 listOne
反过来找 listTwo
独有的元素也很简单:
List<String> differences = new ArrayList<>(listTwo);
differences.removeAll(listOne);
assertEquals(3, differences.size());
assertThat(differences).containsExactly("Daniel", "Alan", "George");
📌 小贴士:如果想求交集,可以用 retainAll()
方法。
4. 使用 Java 8 Streams API
Stream 提供了更函数式、更灵活的处理方式。我们可以用 filter()
筛选出不在另一个列表中的元素:
List<String> differences = listOne.stream()
.filter(element -> !listTwo.contains(element))
.collect(Collectors.toList());
assertEquals(2, differences.size());
assertThat(differences).containsExactly("Tom", "John");
逻辑清晰:保留 listOne
中那些不在 listTwo
里的元素
同样,交换顺序可得反向差集:
List<String> differences = listTwo.stream()
.filter(element -> !listOne.contains(element))
.collect(Collectors.toList());
assertEquals(3, differences.size());
assertThat(differences).containsExactly("Daniel", "Alan", "George");
❌ 踩坑提醒:listTwo.contains(element)
在每次流处理中都会调用,若 listTwo
很大,性能会急剧下降(O(n²))。
✅ 改进建议:先将 listTwo
转为 Set
,提升查找效率:
Set<String> setTwo = new HashSet<>(listTwo);
List<String> differences = listOne.stream()
.filter(e -> !setTwo.contains(e))
.collect(Collectors.toList());
5. 使用第三方库
5.1 Google Guava
Guava 提供了专门处理集合差集的工具方法:Sets.difference(set1, set2)
,简洁明了。
但注意:它只支持 Set
,所以需要先转换:
List<String> differences = new ArrayList<>(
Sets.difference(Sets.newHashSet(listOne), Sets.newHashSet(listTwo))
);
assertEquals(2, differences.size());
assertThat(differences).containsExactlyInAnyOrder("Tom", "John");
⚠️ 缺点:
- 自动去重(
"Jack"
只出现一次) - 不保证顺序
📌 适用场景:你只关心“有哪些不同”,不关心重复和顺序
5.2 Apache Commons Collections
CollectionUtils.removeAll()
方法和 List.removeAll()
类似,但它会返回一个新的集合,避免破坏原数据:
List<String> differences = new ArrayList<>(
CollectionUtils.removeAll(listOne, listTwo)
);
assertEquals(2, differences.size());
assertThat(differences).containsExactly("Tom", "John");
✅ 优点:语义清晰,无需手动 new ArrayList
❌ 缺点:仍会去重,行为基于 Set
逻辑
6. 处理重复元素的差集
前面的方法都忽略了重复值。但在某些业务场景下,重复是有意义的。
比如:listOne
有两个 "Jack",listTwo
只有一个。我们希望差集里保留一个 "Jack",因为只被“抵消”了一个。
方案一:手动遍历 remove
List<String> differences = new ArrayList<>(listOne);
listTwo.forEach(differences::remove); // 每个元素只 remove 一次
assertThat(differences).containsExactly("Tom", "John", "Jack");
✅ 原理:List.remove(Object)
默认只删除第一次匹配的元素
✅ 结果保留了一个 "Jack",符合预期
方案二:Apache Commons 的 subtract 方法
这才是真正支持“带重复项差集”的利器:
List<String> differences = new ArrayList<>(
CollectionUtils.subtract(listOne, listTwo)
);
assertEquals(3, differences.size());
assertThat(differences).containsExactly("Tom", "John", "Jack");
✅ subtract(A, B)
定义为:A 中每个元素减去 B 中出现的次数
✅ 完美支持多重集合(Multiset)语义
📌 推荐:当你需要精确处理重复元素时,首选 CollectionUtils.subtract
7. 总结
方法 | 是否支持重复 | 是否保持顺序 | 推荐场景 |
---|---|---|---|
List.removeAll() |
❌ | ✅ | 简单去重差集 |
Stream + filter |
❌(可优化) | ✅ | 需要链式操作或结合其他逻辑 |
Guava Sets.difference() |
❌ | ❌ | 快速求 Set 差集 |
CollectionUtils.removeAll() |
❌ | ✅ | 替代原生 removeAll,语义更清晰 |
CollectionUtils.subtract() |
✅ | ✅ | 需要保留重复项的差集 ✅ |
📌 最佳实践建议:
- 普通场景:用
Stream + HashSet
提升性能 - 精确去重场景:用
CollectionUtils.subtract
- 快速原型:Guava 的
Sets.difference
也很香
所有示例代码已托管至 GitHub:https://github.com/baeldung/java-tutorials
模块路径:core-java-modules/core-java-collections-list-3