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 元素不同(有重复)

目标很明确:

  • firstsecond 应判定为“无序相等”
  • firstthird 应判定为“不等”

接下来,看看各种实现方案。

3. 使用 JUnit 原生断言

JUnit 是 Java 单元测试的基石。虽然它没有直接提供“无序相等”断言,但我们可以通过组合逻辑手动实现。

基本思路

判断两个 List 无序相等,需同时满足:

  1. 大小相同
  2. A 包含 B 的所有元素
  3. 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));
}

虽然 firstthird 大小相同,但元素内容不同(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); // ❌ 测试竟然通过了!
}

尽管 ab 多了一个 "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-exampleslist-equality 模块中找到。


原始标题:Assert Two Lists for Equality Ignoring Order in Java