如何在 Java 中对 List 进行排序:list.sort、Comparator、多条件及空值处理

目次

1. 本文将教会你什么(排序 Java List 的最简方法)

在使用 Java 时,List 进行排序 是极其常见的需求。
与此同时,许多开发者——尤其是初学者——常常被以下问题困扰:

  • 我应该使用哪种方法来排序 List?
  • list.sort()Collections.sort() 有何区别?
  • 如何对对象列表而不是简单值进行排序?

本文旨在 清晰且实用 地回答这些问题,先给出结论,再逐步解释细节和实际使用场景。

1.1 结论:只需记住这一种模式

如果你只想要 最简且最标准的 Java List 排序方式,就是下面这样:

list.sort(Comparator.naturalOrder());

这会以 升序(从小到大、A 到 Z、从旧到新)对 List 进行排序。

如果想要 降序,使用下面的代码:

list.sort(Comparator.reverseOrder());

仅凭这两行代码,你已经可以对以下类型的 List 进行排序:

  • Integer
  • String
  • LocalDate
  • 以及大多数其他常见类型

对于日常的大多数场景,这已经足够。

1.2 对对象列表进行排序:指定排序键

在真实项目中,List 往往存放的是 对象,而不是简单值。

例如:

class Person {
    private String name;
    private int age;

    // getters omitted
}

要按年龄对 List<Person> 进行排序,只需写:

list.sort(Comparator.comparing(Person::getAge));

这行代码的含义是:

  • 从每个 Person 中提取 age
  • 对这些值进行比较
  • 以升序方式对 List 排序

你无需手动实现比较逻辑。只要记住下面的模式:

Comparator.comparing(要比较的属性)

1.3 本文覆盖的内容

本文从基础到实战,系统讲解 Java List 排序,包括:

  • list.sort()Collections.sort() 的区别
  • Comparator 的简明工作原理
  • 多条件排序(例如:年龄 → 姓名)
  • 同时使用升序和降序
  • 安全处理 null 值的方法
  • 何时使用 stream().sorted() 替代 list.sort()

目标不是让你死记硬背,而是 理解每种方式背后的原因

1.4 适合阅读本文的对象

本文面向的读者是:

  • 已掌握基本的 Java 语法(类、方法、List)
  • 使用过 ArrayListList 的开发者
  • 每次需要排序时仍然要去查找代码示例的程序员

本文 针对从未写过 Java 代码的绝对初学者,但对想快速掌握实用 Java 编程的开发者仍然友好。

2. Java 中对 List 排序的三种常见方式(该选哪一种?)

在 Java 中,对 List 排序并非只有唯一的做法。实际工作中,开发者主要使用 三种不同的方案,它们在行为和意图上略有差别。

在深入 Comparator 之前,先了解 每种方案出现的时机和原因

2.1 list.sort(Comparator) — 现代且推荐的做法

自 Java 8 起,这已经成为 标准且最常推荐的 List 排序方式

list.sort(Comparator.naturalOrder());

关键特性

  • 直接定义在 List 接口上
  • 代码简洁、可读性高
  • 在原地修改 List(破坏性)

对对象的排序方式同样适用:

list.sort(Comparator.comparing(Person::getAge));

何时使用

  • 你可以接受修改原始 List
  • 你希望得到最清晰、最简洁的实现

👉 如果不确定该选哪种,list.sort() 往往是最佳选择。

2.2 Collections.sort(list) — 仍需了解的旧式写法

在较老的教程或遗留项目中,你可能会看到如下代码:

Collections.sort(list);

或者配合 Comparator 使用:

Collections.sort(list, Comparator.reverseOrder());

关键特性

  • 自 Java 1.2 起存在
  • 内部行为几乎与 list.sort 相同
  • 也会修改原始 List

为什么它在今天不太常用

  • Java 8 引入了 list.sort,使用起来更自然
  • 通过 Collections 对 List 进行排序不够直观

对于新代码,推荐使用 list.sort。然而,在阅读旧代码库时,了解 Collections.sort 仍然很重要。

2.3 stream().sorted() — 非破坏性排序

第三种方式使用 Stream API。

List<Integer> sorted =
    list.stream()
        .sorted()
        .toList();

使用 Comparator:

List<Person> sorted =
    list.stream()
        .sorted(Comparator.comparing(Person::getAge))
        .toList();

关键特性

  • 不会修改原始 List
  • 返回一个新的已排序 List
  • 易于与 filtermap 以及其他流操作组合使用

何时使用

  • 当必须保持原始 List 不变时
  • 当排序是数据处理管道的一部分时

然而,对于简单的排序,list.sort 通常更清晰且更高效。

2.4 如何选择(快速决策指南)

GoalRecommended Method
Sort a List directlylist.sort()
Understand or maintain old codeCollections.sort()
Keep the original List unchangedstream().sorted()

此时,请记住唯一的规则:

除非有明确的理由,否则请使用 list.sort()

3. 基础排序:升序和降序

现在你已经了解 使用哪种排序方法,让我们关注最常见的需求:升序或降序排序

本节涵盖数字、字符串和日期等基本类型的 List——后续所有内容的基础。

3.1 升序排序(自然顺序)

许多 Java 类型都有 自然顺序,例如:

  • 数字 → 从小到大
  • 字符串 → 按字母顺序(A 到 Z)
  • 日期 → 从早到晚

要对 List 进行升序排序,使用:

list.sort(Comparator.naturalOrder());

示例:数字排序

List<Integer> numbers = Arrays.asList(5, 1, 3, 2);
numbers.sort(Comparator.naturalOrder());

结果:

[1, 2, 3, 5]

示例:字符串排序

List<String> names = Arrays.asList("Tom", "Alice", "Bob");
names.sort(Comparator.naturalOrder());

结果:

[Alice, Bob, Tom]

👉 如果只需要升序,这是最简单且最安全的方法。

3.2 降序排序

要反转自然顺序,使用 Comparator.reverseOrder()

list.sort(Comparator.reverseOrder());

示例:数字降序

List<Integer> numbers = Arrays.asList(5, 1, 3, 2);
numbers.sort(Comparator.reverseOrder());

结果:

[5, 3, 2, 1]

示例:字符串降序

List<String> names = Arrays.asList("Tom", "Alice", "Bob");
names.sort(Comparator.reverseOrder());

结果:

[Tom, Bob, Alice]

3.3 当可以省略 Comparator 时

在某些情况下,你可能会看到没有显式 Comparator 的排序代码。

Collections.sort(list);

甚至是:

list.sort(null);

这些仅在以下情况下有效 仅当

  • 元素实现了 Comparable 接口
  • 你想要自然(升序)顺序

虽然有效,但这些写法不够明确。

在实际代码库中,通常更倾向于使用以下更清晰的写法:

list.sort(Comparator.naturalOrder());

3.4 常见误解:谁决定排序顺序?

初学者常常误以为:

sort() 决定升序或降序

实际上:

  • sort() 执行排序
  • Comparator 定义了元素的比较方式

一旦理解了这种职责分离,其他所有内容——对象排序、多条件以及 null 处理——都将变得更容易。

4. 对象列表排序:理解 Comparator

在实际的 Java 应用中,你很少会对 IntegerString 这类简单值进行排序。大多数情况下,你会对 自定义对象的 List 进行排序。

这就是 Comparator 成为核心概念的地方。

4.1 Comparator 是什么?

Comparator 定义了 两个元素应如何比较

从概念上讲,它回答了以下问题:

“给定两个对象,哪个应该排在前面?”

在内部,Comparator 返回:

  • 一个 负数 → 第一个元素排在前面
  • → 顺序无关紧要
  • 一个 正数 → 第二个元素排在前面

幸运的是,在现代 Java 中,你几乎不需要手动实现这段逻辑。

4.2 使用 Comparator.comparing 按字段排序

考虑以下类:

class Person {
    private String name;
    private int age;

    // getters omitted
}

要按年龄对 List<Person> 进行排序,使用:

list.sort(Comparator.comparing(Person::getAge));

这段代码的含义很自然:

  • 取每个 Person
  • 提取 age
  • 比较这些值
  • 按升序对 List 进行排序

这一行代码取代了以前需要多行的比较代码。

4.3 按字符串、日期及其他类型排序

相同的模式几乎适用于任何字段类型。

按名称排序

list.sort(Comparator.comparing(Person::getName));

按日期排序

list.sort(Comparator.comparing(Person::getBirthDate));

只要提取的值具有自然顺序,Comparator.comparing 就能在无需额外设置的情况下工作。

4.4 使用针对原始类型的 Comparator

对于数值字段,Java 提供了优化的方法:

  • comparingInt
  • comparingLong
  • comparingDouble

示例:

list.sort(Comparator.comparingInt(Person::getAge));

这些方法:

  • 避免不必要的对象装箱
  • 使意图更清晰
  • 对大 List 来说稍微更高效

虽然差异不大,但它们被视为数值字段的最佳实践。

4.5 为什么这很重要

一旦你理解了 Comparator.comparing,就可以实现:

  • 多条件排序
  • 升序和降序混合排序
  • 安全处理 null

换句话说,这就是 Java 实际 List 排序的基础。

5. 多条件排序(最常见的实际模式)

在实际应用中,仅按单个字段排序往往不够。通常需要 二级和三级条件 来创建稳定且有意义的顺序。

例如:

  • 先按年龄排序,再按名称排序
  • 先按优先级排序,再按时间戳排序
  • 先按分数(降序)排序,再按 ID(升序)排序

Java 的 Comparator API 正是为此而设计的。

5.1 thenComparing 的基础

多条件排序遵循一个简单规则:

如果两个元素在第一个条件上相等,则使用下一个条件。

下面是基本模式:

list.sort(
    Comparator.comparingInt(Person::getAge)
              .thenComparing(Person::getName)
);

这意味着:

  1. age(升序)排序
  2. 如果年龄相等,则按 name(升序)排序

这会产生一致且可预测的顺序。

5.2 混合升序和降序

经常会出现一个字段需要降序,另一个字段需要升序的情况。

示例:分数(降序),名称(升序)

list.sort(
    Comparator.comparingInt(Person::getScore).reversed()
              .thenComparing(Person::getName)
);

重要细节:

  • reversed() 只作用于它前面的 Comparator

这使得组合不同的排序方向变得安全。

5.3 将 Comparator 传递给 thenComparing

为了更好的可读性,你可以在 thenComparing 中显式定义排序方向。

示例:年龄(升序),注册日期(降序)

list.sort(
    Comparator.comparingInt(Person::getAge)
              .thenComparing(
                  Comparator.comparing(Person::getRegisterDate).reversed()
              )
);

这种风格非常清晰地显示哪些字段是升序或降序,
这对代码审查和长期维护很有帮助。

5.4 一个真实的业务示例

list.sort(
    Comparator.comparingInt(Order::getPriority)
              .thenComparing(Order::getDeadline)
              .thenComparing(Order::getOrderId)
);

排序逻辑:

  1. 优先级更高的排在前面
  2. 截止日期更早的排在前面
  3. 订单 ID 更小的排在前面

这确保了排序的稳定性并符合业务需求。

5.5 保持多条件排序的可读性

随着排序逻辑的增长,可读性比简洁性更重要。

最佳实践:

  • 为每个条件换行
  • 避免深度嵌套的 lambda 表达式
  • 如果业务规则不明显,添加注释

清晰的排序逻辑为以后阅读代码的所有人节省时间。

6. 安全处理 null 值(非常常见的错误来源)

在对真实数据进行排序时,null 值几乎不可避免
字段可能是可选的,旧数据可能不完整,或者值可能根本缺失。

如果不显式处理 null,排序很容易在运行时失败。

6.1 为什么 null 在排序时会导致问题

考虑以下代码:

list.sort(Comparator.comparing(Person::getName));

如果 getName() 对任何元素返回 null,Java 在比较时会抛出
NullPointerException

出现这种情况的原因是:

  • Comparator 假设值可以比较
  • 除非你定义,否则 null 没有自然顺序

因此,必须显式处理 null

6.2 使用 nullsFirstnullsLast

Java 提供了帮助方法来定义 null 值的排序方式。

null 值放在前面

list.sort(
    Comparator.comparing(
        Person::getName,
        Comparator.nullsFirst(Comparator.naturalOrder())
    )
);

null 值放在后面

list.sort(
    Comparator.comparing(
        Person::getName,
        Comparator.nullsLast(Comparator.naturalOrder())
    )
);

这些做法:

  • 防止 NullPointerException
  • 使排序规则明确且易读

6.3 当列表本身包含 null 元素时

有时,列表的元素本身 可能是 null

List<Person> list = Arrays.asList(
    new Person("Alice", 20),
    null,
    new Person("Bob", 25)
);

安全处理方法如下:

list.sort(
    Comparator.nullsLast(
        Comparator.comparing(Person::getName)
    )
);

这确保了:

  • null 元素被移动到末尾
  • null 元素正常排序

6.4 小心使用 comparingIntnull

comparingInt 这样的原始类型比较器 无法处理 null

Comparator.comparingInt(Person::getAge); // age must be int

如果该字段是可能为 nullInteger,请使用:

Comparator.comparing(
    Person::getAge,
    Comparator.nullsLast(Integer::compare)
);

这可以避免意外的运行时错误。

6.5 将 null 处理视为规范的一部分

决定 null 值是否出现的方式:

  • 在开头
  • 在末尾
  • 或者完全过滤掉

是一个 业务决策,而不仅仅是技术决定。

通过使用 nullsFirstnullsLast,你可以直接在代码中记录该决策——
使排序逻辑更安全、更易于理解。

7. 常见陷阱和错误(如何避免细微的 Bug)

在 Java 中对 List 进行排序看似简单,但存在若干 容易忽视的陷阱,可能导致 Bug、意外行为或性能问题。

提前了解这些内容可以在调试和代码审查时为你节省时间。

7.1 忘记排序是破坏性的

Both list.sort() and Collections.sort() 会修改原始 List.

List<Integer> original = new ArrayList<>(List.of(3, 1, 2));
List<Integer> alias = original;

original.sort(Comparator.naturalOrder());

在这种情况下:

  • original 已排序
  • alias 也已排序(因为它们引用同一个 List)

如何避免这种情况

如果需要保留原始顺序:

List<Integer> sorted = new ArrayList<>(original);
sorted.sort(Comparator.naturalOrder());

或者使用流:

List<Integer> sorted =
    original.stream()
            .sorted()
            .toList();

时刻自问:
“修改原始 List 可以吗?”

7.2 Comparator 一致性与稳定排序

Comparator 应该产生 一致且可预测的结果

示例:

Comparator.comparing(Person::getAge);

如果多个人的年龄相同,它们的相对顺序是未定义的。

这在某些情况下可以接受——但通常不行。

最佳实践

添加次要条件以稳定顺序:

Comparator.comparingInt(Person::getAge)
          .thenComparing(Person::getId);

这确保排序结果是确定的。

7.3 字符串排序中的大小写敏感性

String 的自然顺序是 区分大小写 的。

List<String> list = List.of("apple", "Banana", "orange");
list.sort(Comparator.naturalOrder());

这可能产生不直观的结果。

不区分大小写的排序

list.sort(String.CASE_INSENSITIVE_ORDER);

在选择之前,考虑:

  • 这用于显示吗?
  • 还是用于内部逻辑?

答案决定了正确的做法。

7.4 在 Comparator 中进行繁重工作

在排序过程中,Comparator 可能被 多次 调用。

避免:

  • 数据库访问
  • 网络调用
  • 高开销的计算
    // Bad idea (conceptual example)
    Comparator.comparing(p -> expensiveOperation(p));
    

更好的做法

  • 预先计算值
  • 将其存入字段
  • 比较简单、低成本的值

高效的 Comparator 在处理大 List 时差别巨大。

7.5 优先考虑可读性而非巧妙

排序逻辑往往被阅读的次数多于编写的次数。

而不是:

  • 一个冗长的链式表达式
  • 深度嵌套的 lambda

更倾向于:

  • 换行
  • 清晰的结构
  • 可选的业务规则注释

可读的排序代码能减少 bug 并使维护更容易。

8. 性能考虑与正确方法的选择

到目前为止,你已经了解了在 Java 中 如何 对 List 进行排序。
本节侧重于从性能和设计角度 选择哪种方法

在大多数应用中,排序不是瓶颈——但不当的选择仍可能导致不必要的开销。

8.1 list.sort()stream().sorted()

这是最常见的决策点。

list.sort()

list.sort(Comparator.comparingInt(Person::getAge));

优点

  • 没有额外的 List 分配
  • 意图明确:“对该 List 排序”
  • 稍微更高效

缺点

  • 会修改原始 List

stream().sorted()

List<Person> sorted =
    list.stream()
        .sorted(Comparator.comparingInt(Person::getAge))
        .toList();

优点

  • 原始 List 保持不变
  • 自然地融入流管道

缺点

  • 会分配一个新 List
  • 稍有额外开销

实用规则

  • 简单排序list.sort()
  • 转换管道或需要不可变stream().sorted()

8.2 高效排序大 List

排序算法会多次调用 Comparator 很多次。 对于大 List,这一点很重要。

关键指南

  • 保持 Comparator 轻量
  • 避免进行繁重工作的链式调用
  • 优先使用原始类型 Comparator(comparingInt 等)

示例:预先计算昂贵的键

而不是:

Comparator.comparing(p -> calculateScore(p));

这样做:

// Precompute once
p.setScore(calculateScore(p));

然后按字段排序:

Comparator.comparingInt(Person::getScore);

这大大减少了排序过程中的重复工作。

8.3 Collections.sort() 是否曾经是正确选择?

对于新代码,几乎从不。

然而,它仍然出现在:

  • 遗留项目
  • 较旧的教程
  • Java 7 及更早的代码库

你不需要使用它——但你应该认识它。

8.4 推荐决策检查清单

在排序之前,问自己:

  1. 我可以修改原始 List 吗?
  2. 我需要多个排序条件吗?
  3. 任何字段可以是 null 吗?
  4. 性能在大规模时重要吗?

回答这些问题自然会引导你到正确的解决方案。

9. 总结:Java List 排序速查表

让我们将所有内容总结成一个快速参考指南,你可以依赖它。

9.1 你最常使用的快速模式

升序

list.sort(Comparator.naturalOrder());

降序

list.sort(Comparator.reverseOrder());

9.2 按字段排序对象

list.sort(Comparator.comparing(Person::getName));

对于数字:

list.sort(Comparator.comparingInt(Person::getAge));

9.3 多个条件

list.sort(
    Comparator.comparingInt(Person::getAge)
              .thenComparing(Person::getName)
);

混合顺序:

list.sort(
    Comparator.comparingInt(Person::getScore).reversed()
              .thenComparing(Person::getName)
);

9.4 安全的 null 处理

list.sort(
    Comparator.comparing(
        Person::getName,
        Comparator.nullsLast(Comparator.naturalOrder())
    )
);

List 可能包含 null 元素:

list.sort(
    Comparator.nullsLast(
        Comparator.comparing(Person::getName)
    )
);

9.5 非破坏性排序

List<Person> sorted =
    list.stream()
        .sorted(Comparator.comparingInt(Person::getAge))
        .toList();

9.6 最终要点

一旦你记住以下内容,
Java List 排序就会变得简单:

  • Comparator 定义顺序
  • sort() 执行操作
  • 清晰胜过巧妙
  • 显式 null 处理防止 bug

如果你内化这些原则,
你就再也不需要“重新学习” Java List 排序了。