1. 概述

在实际开发中,我们经常需要比较两个同类型对象集合的差异。比如,有一个参加考试的学生名单,还有一个通过考试的学生名单,两者之间的差集就是未通过考试的学生

Java 的 List 接口本身没有直接提供“求差集”的方法,但我们可以借助一些 API 或第三方工具类来实现。本文将带你用几种不同的方式实现这一需求:包括原生 Java(传统方式和 Stream)、以及 Guava 和 Apache Commons Collections 等常用库。

✅ 目标明确:找出 list1 - list2list2 - 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


原始标题:Finding the Differences Between Two Lists in Java | Baeldung