教程

如何在 Java 中比较两个列表(相同、缺失和相等的元素)

作者 10分钟阅读

在 Java 中比较两个列表是很常见的需求:同步数据库记录、在测试中检查接口返回值、找出两次导出之间新增或删除了哪些 ID。根据问题的不同,需要用不同的方法:

  • 两个列表是否相等?——考虑或不考虑顺序和重复项
  • 哪些元素两个列表都有?——交集
  • 哪些元素只在一个列表里?——差集

本文用标准库(Stream.toList() 和 record 需要 Java 16+)讲解每种情况,并给出 Apache Commons 和 Guava 的替代写法。所有代码片段都使用以下导入:

import java.util.*;
import java.util.stream.*;

只需要比较一次两个列表?把它们粘贴到免费的 两个列表比较工具,无需写代码就能看到仅在 A、仅在 B 和两者共有的项目。


1. 判断两个列表是否相等

只有当两个列表以相同顺序包含相同元素时,List.equals() 才返回 true:

List<Integer> a = List.of(1, 2, 3);
List<Integer> b = List.of(1, 2, 3);
List<Integer> c = List.of(3, 2, 1);

System.out.println(a.equals(b)); // true
System.out.println(a.equals(c)); // false – same elements, different order

它用元素自身的 equals() 方法比较,所以适用于字符串、数字以及正确实现了 equals() 的任何类。它也能跨实现类工作:元素相同的 ArrayList 和 LinkedList 是相等的。


2. 忽略顺序比较列表

忽略顺序的方式取决于重复项是否有意义。

元素相同且出现次数相同(重复项有意义)——把副本排序后比较:

List<Integer> x = Arrays.asList(3, 1, 2, 2);
List<Integer> y = Arrays.asList(2, 3, 2, 1);

List<Integer> sortedX = new ArrayList<>(x);
List<Integer> sortedY = new ArrayList<>(y);
Collections.sort(sortedX);
Collections.sort(sortedY);

System.out.println(sortedX.equals(sortedY)); // true

如果元素无法排序,就比较频次映射:

Map<Integer, Long> countX = x.stream()
        .collect(Collectors.groupingBy(i -> i, Collectors.counting()));
Map<Integer, Long> countY = y.stream()
        .collect(Collectors.groupingBy(i -> i, Collectors.counting()));

System.out.println(countX.equals(countY)); // true

不同元素相同(忽略重复项)——比较集合:

List<Integer> z = List.of(1, 2, 3);
System.out.println(new HashSet<>(x).equals(new HashSet<>(z))); // true

3. 用 retainAll 找出共同元素(交集)

retainAll() 会移除所有不在另一个集合中的元素。它会修改调用它的列表,所以要在副本上操作:

List<String> listA = List.of("apple", "banana", "cherry", "date");
List<String> listB = List.of("banana", "date", "fig");

List<String> common = new ArrayList<>(listA);
common.retainAll(new HashSet<>(listB));

System.out.println(common); // [banana, date]

复制到新的 ArrayList 还有一个原因:用 List.of() 或 Arrays.asList() 创建的列表不支持删除,对它们调用 retainAll() 会抛出 UnsupportedOperationException。


4. 用 removeAll 找出缺失元素(差集)

removeAll() 正好相反:它会移除所有在另一个集合中的元素,留下第一个列表独有的元素:

List<String> onlyInA = new ArrayList<>(listA);
onlyInA.removeAll(new HashSet<>(listB));
System.out.println(onlyInA); // [apple, cherry]

List<String> onlyInB = new ArrayList<>(listB);
onlyInB.removeAll(new HashSet<>(listA));
System.out.println(onlyInB); // [fig]

这两个方法都会保留起始列表的原有顺序,留下来的元素如果有重复也会保留。


5. 用 Stream 实现

Stream 不会修改源列表,需要额外过滤条件时写起来也很清晰:

Set<String> setB = new HashSet<>(listB);

List<String> onlyInA = listA.stream()
        .filter(item -> !setB.contains(item))
        .toList();                       // Java 16+; use collect(Collectors.toList()) before that

List<String> inBoth = listA.stream()
        .filter(setB::contains)
        .toList();

System.out.println(onlyInA); // [apple, cherry]
System.out.println(inBoth);  // [banana, date]

要得到只在其中一个列表中的元素(对称差),把两个方向的差集合并起来:

Set<String> setA = new HashSet<>(listA);

List<String> inOnlyOne = Stream.concat(
        listA.stream().filter(item -> !setB.contains(item)),
        listB.stream().filter(item -> !setA.contains(item)))
        .toList();

System.out.println(inOnlyOne); // [apple, cherry, fig]

如果要忽略大小写,在构建集合前先统一格式——例如 listB.stream().map(String::toLowerCase).collect(Collectors.toSet())——检查时也把每一项转成小写。


6. 比较对象列表

集合用 equals() 比较对象,基于哈希的集合还会用到 hashCode()。Java record 会根据字段自动生成这两个方法,所以可以直接使用:

record Customer(int id, String name) {}

List<Customer> oldCustomers = List.of(new Customer(1, "Ann"), new Customer(2, "Bob"));
List<Customer> newCustomers = List.of(new Customer(2, "Bob"), new Customer(3, "Cy"));

Set<Customer> newSet = new HashSet<>(newCustomers);
List<Customer> removed = oldCustomers.stream()
        .filter(c -> !newSet.contains(c))
        .toList();

System.out.println(removed); // [Customer[id=1, name=Ann]]

对于普通类,要同时重写 equals() 和 hashCode()——如果只重写 equals(),HashSet 查找时将找不到匹配的对象。

很多时候你希望按某个键而不是全部字段来比较对象,这样客户改了名字也不会被当成另一个客户:

Set<Integer> newIds = newCustomers.stream()
        .map(Customer::id)
        .collect(Collectors.toSet());

List<Customer> removedById = oldCustomers.stream()
        .filter(c -> !newIds.contains(c.id()))
        .toList();

System.out.println(removedById); // [Customer[id=1, name=Ann]]

7. 工具库:Apache Commons 和 Guava

如果项目里已经在用这些库,可以一行代码完成集合运算。

Apache Commons Collections(org.apache.commons.collections4.CollectionUtils):

Collection<String> onlyInA = CollectionUtils.subtract(listA, listB);     // [apple, cherry]
Collection<String> inBoth = CollectionUtils.intersection(listA, listB);  // banana, date (any order)
Collection<String> inOnlyOne = CollectionUtils.disjunction(listA, listB); // apple, cherry, fig (any order)

Guava(com.google.common.collect.Sets)作用于集合,返回的是实时视图:

Set<String> setA = new HashSet<>(listA);
Set<String> setB = new HashSet<>(listB);

Set<String> onlyInA = Sets.difference(setA, setB);
Set<String> inBoth = Sets.intersection(setA, setB);
Set<String> inOnlyOne = Sets.symmetricDifference(setA, setB);

CollectionUtils 会考虑每个元素出现的次数;Guava 的 Sets 只处理不同的元素。如果底层集合之后会变化,可以用 new HashSet<>(view) 复制一份 Guava 视图。


8. 性能:不要在循环里查找 List

list.contains() 会逐个检查元素,所以 ArrayList.removeAll(otherList) 和 filter(otherList::contains) 会把一个列表的每个元素与另一个列表的每个元素都比较一遍。两个各有 10 万项的列表,最多可能需要 100 亿次比较。

像上面所有示例那样,先把另一个列表包装成 HashSet,每次查找就变成常数时间,整个比较大约只需 10 万步:

// Slow: O(n × m)
listA.stream().filter(listB::contains).toList();

// Fast: O(n + m)
Set<String> lookup = new HashSet<>(listB);
listA.stream().filter(lookup::contains).toList();

总结

目标 推荐做法 保留顺序? 说明
相等,顺序相同 a.equals(b) – 可跨 List 实现类比较
相等,任意顺序,考虑重复 排序副本或比较频次映射 – 映射适用于无法排序的元素
相等,任意顺序,不考虑重复 new HashSet<>(a).equals(new HashSet<>(b)) – 忽略重复元素
共同元素 copy.retainAll(new HashSet<>(b)) 是 先复制;List.of 不可修改
仅在 A 中 copy.removeAll(new HashSet<>(b)) 或 Stream 过滤 是 Stream 写法不修改任何列表
只在一个列表中 用 Stream.concat 合并两个差集 是 或用 CollectionUtils.disjunction
对象列表 record,或 equals() + hashCode() 是 或按 id 等键比较

先从 HashSet 查找加 Stream 过滤开始——速度快,不修改列表,几乎能覆盖所有情况。在代码之外快速检查一次的话,在线列表比较工具 可以在浏览器里完成同样的事。在用 Python?请看 如何在 Python 中比较两个列表。

标签

#Java#列表比较#教程

关于作者

songshiqiang独立开发者

使用 AI 工具开发有趣的网站。

相关文章

教程

如何在 Google 表格中比较两个列表(找出相同项和差异)

用条件格式、COUNTIF、MATCH、XLOOKUP 和 FILTER 在 Google 表格中比较两个列表:高亮匹配项、标记缺失项、把差异提取成新列表,以及比较不同工作表中的列表。

9分钟阅读
#Google 表格#列表比较#教程
阅读更多
教程

如何在 Python 中比较两个列表(附示例)

Python 比较两个列表的完整指南:用集合找出相同和不同的元素,用列表推导式保留顺序,判断两个列表是否相等,逐个元素比较,比较字典列表,以及用 pandas 处理大数据。

12分钟阅读
#Python#列表比较#教程
阅读更多
教程

如何在 Excel 中比较两个列表(找出相同项和差异)

在 Excel 中比较两个列表的 9 种方法:用条件格式高亮匹配项,用 COUNTIF 或 MATCH 标记,用 VLOOKUP 或 XLOOKUP 查找,用 FILTER 提取,逐行比较,以及用 Power Query 处理大数据。

12分钟阅读
#Excel#列表比较#XLOOKUP#教程
阅读更多