1. 概述
写单元测试时,经常会遇到需要判断两个 List
是否包含相同元素,但不关心元素顺序的场景。这种“无序相等”判断在验证接口返回、数据处理结果时尤为常见。
本文将系统介绍几种在 Java 中实现 List 无序比较的主流方式,涵盖 JUnit 原生断言、AssertJ、Hamcrest 和 Apache Commons Collections 等常用工具,帮你避开“顺序坑”,写出更健壮的测试。
2. 示例数据准备
根据 Java 官方文档,List#equals()
要求元素顺序完全一致才算相等。因此直接用 equals()
无法满足我们的需求。
我们以下面三个 List
作为测试数据,贯穿全文:
List<Integer> first = Arrays.asList(1, 3, 4, 6, 8);
List<Integer> second = Arrays.asList(8, 1, 6, 3, 4); // 与 first 元素相同,顺序不同
List<Integer> third = Arrays.asList(1, 3, 3, 6, 6); // 与 first 元素不同(有重复)
目标很明确:
- ✅
first
和second
应判定为“无序相等” - ❌
first
和third
应判定为“不等”
接下来,看看各种实现方案。
3. 使用 JUnit 原生断言
JUnit 是 Java 单元测试的基石。虽然它没有直接提供“无序相等”断言,但我们可以通过组合逻辑手动实现。
基本思路
判断两个 List 无序相等,需同时满足:
- 大小相同
- A 包含 B 的所有元素
- B 包含 A 的所有元素
示例代码
@Test
public void whenTestingForOrderAgnosticEquality_ShouldBeTrue() {
assertTrue(first.size() == second.size()
&& first.containsAll(second)
&& second.containsAll(first));
}
这个方案 ✅ 能工作,但 ❌ 可读性差,断言语句又长又绕,一眼看不出在测什么。
再看一个失败的 case:
@Test
public void whenTestingForOrderAgnosticEquality_ShouldBeFalse() {
assertFalse(first.size() == third.size()
&& first.containsAll(third)
&& third.containsAll(first));
}
虽然 first
和 third
大小相同,但元素内容不同(third
有重复的 3 和 6),所以 containsAll
会失败,断言通过。
⚠️ 踩坑提示:这种方式虽然有效,但代码丑陋,建议仅作为“没有引入其他库”时的备选方案。
4. 使用 AssertJ
AssertJ 以其流畅的链式 API 著称,能极大提升测试代码的可读性和表达力。
引入依赖
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.24.2</version> <!-- 推荐使用最新稳定版 -->
<scope>test</scope>
</dependency>
方案一:hasSameElementsAs()
❌(慎用)
@Test
void whenTestingForOrderAgnosticEqualityBothList_ShouldBeEqual() {
assertThat(first).hasSameElementsAs(second);
}
这个方法看似完美,但它有一个致命缺陷:会忽略重复元素!
看这个反例:
@Test
void whenTestingForOrderAgnosticEqualityBothList_ShouldNotBeEqual() {
List<String> a = Arrays.asList("a", "a", "b", "c");
List<String> b = Arrays.asList("a", "b", "c");
assertThat(a).hasSameElementsAs(b); // ❌ 测试竟然通过了!
}
尽管 a
比 b
多了一个 "a"
,但 hasSameElementsAs
认为它们“元素种类”相同,就判定相等。这在大多数业务场景下是 严重 bug。
方案二:containsExactlyInAnyOrderElementsOf()
✅(推荐)
要精确比较(包括重复次数),必须使用:
@Test
void whenTestingForOrderAgnosticEquality_ShouldRespectDuplicates() {
List<String> a = Arrays.asList("a", "a", "b", "c");
List<String> b = Arrays.asList("a", "b", "c");
assertThat(a).containsExactlyInAnyOrderElementsOf(b); // ✅ 测试失败,符合预期
}
这个方法会严格校验:
- 元素是否完全相同
- 每个元素的出现次数是否一致
- 不关心顺序
✅ 强烈推荐在需要精确比较时使用此方法,语义清晰,不易踩坑。
5. 使用 Hamcrest
Hamcrest 提供了强大的 Matcher 机制,其集合匹配器非常适合做无序比较。
引入依赖
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId>
<version>2.2</version>
<scope>test</scope>
</dependency>
⚠️ 注意:
hamcrest-all
已过时,推荐直接引入hamcrest
。
示例代码
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
@Test
public void whenTestingForOrderAgnosticEquality_ShouldBeEqual() {
assertThat(first, containsInAnyOrder(second.toArray()));
}
containsInAnyOrder
Matcher 会自动处理大小和元素内容的比较,无需手动检查 size,逻辑简洁。
它同样能正确处理重复元素:
@Test
public void whenTestingForOrderAgnosticEquality_ShouldRespectDuplicates() {
List<String> a = Arrays.asList("a", "a", "b", "c");
List<String> b = Arrays.asList("a", "b", "c");
assertThat(a, containsInAnyOrder(b.toArray())); // ✅ 测试失败
}
✅ Hamcrest 的方案简单粗暴,适合喜欢声明式风格的开发者。
6. 使用 Apache Commons Collections
如果你的项目已经引入了 Apache Commons,可以直接使用 CollectionUtils
。
引入依赖
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
<scope>test</scope>
</dependency>
示例代码
import org.apache.commons.collections4.CollectionUtils;
@Test
public void whenTestingForOrderAgnosticEquality_ShouldBeTrueIfEqualOtherwiseFalse() {
assertTrue(CollectionUtils.isEqualCollection(first, second)); // ✅ 相等
assertFalse(CollectionUtils.isEqualCollection(first, third)); // ✅ 不等
}
isEqualCollection
方法的定义非常精准:
判断两个集合是否包含完全相同的元素,且每个元素的出现次数(基数,cardinality)也相同。
这正是我们想要的“无序且计重”比较,语义明确,一行代码搞定。
7. 总结
方案 | 优点 | 缺点 | 推荐指数 |
---|---|---|---|
JUnit 原生 | 无需额外依赖 | 代码冗长,可读性差 | ⭐⭐ |
AssertJ | API 流畅,语义强 | 需引入新依赖 | ⭐⭐⭐⭐⭐ |
Hamcrest | Matcher 模式灵活 | 语法稍显特殊 | ⭐⭐⭐⭐ |
Apache Commons | 方法语义精准 | 依赖较重 | ⭐⭐⭐⭐ |
✅ 最终建议:
- 如果项目已用 AssertJ,首选
containsExactlyInAnyOrderElementsOf()
。 - 如果追求简洁,Hamcrest 的
containsInAnyOrder
也很不错。 - 避免使用 AssertJ 的
hasSameElementsAs()
,除非你明确不需要比较重复元素。
所有示例代码均可在 GitHub 仓库
java-testing-examples
的list-equality
模块中找到。