1. 概述

在面向对象编程中,我们经常需要对对象集合进行排序。更进一步地,业务场景往往要求多字段组合排序——比如先按姓名升序,再按年龄降序。这类需求看似简单,但处理不好容易踩坑。

Kotlin 提供了多种原生支持来优雅实现多字段排序,无需依赖外部工具类或手写复杂的比较逻辑。本文将系统梳理这些方法,帮助你在实际开发中快速选出最合适的方案。

2. 使用 Comparable 接口

最直接的方式是让数据类实现 Comparable 接口,在 compareTo() 方法中定义默认的排序规则。这种方式适合有“自然排序”需求的场景。

来看一个例子:定义 Student 类并实现 Comparable<Student>

data class Student(val name: String, val age: Int, val country: String? = null) : Comparable<Student> {
    override fun compareTo(other: Student): Int {
        return compareValuesBy(this, other, { it.name }, { it.age })
    }
}

✅ 上述代码表示:优先按 name 升序,若 name 相同则按 age 升序。

准备测试数据验证效果:

private val students = listOf(
    Student(name = "C", age = 9),
    Student(name = "A", age = 11, country = "C1"),
    Student(name = "B", age = 10, country = "C2"),
    Student(name = "A", age = 10),
)

预期排序结果为:

val studentsSortedByNameAndAge = listOf(
    Student(name = "A", age = 10),
    Student(name = "A", age = 11, country = "C1"),
    Student(name = "B", age = 10, country = "C2"),
    Student(name = "C", age = 9),
)

测试是否符合预期:

assertEquals(
    studentsSortedByNameAndAge,
    students.sorted()
)

⚠️ 注意:一旦实现了 Comparable,该排序逻辑就成为类的默认行为,灵活性受限。如果需要多种排序策略(如有时按年龄、有时按国家),就不适合用此方式。

3. 使用 Kotlin 内建排序函数

当无法修改目标类,或需要动态选择排序策略时,应使用 Kotlin 标准库提供的高阶函数。它们更加灵活,支持链式调用和空值处理。

3.1 使用 sortedWithcompareBy

sortedWith() 配合 compareBy() 是最常用的组合,适用于不可变集合(返回新列表):

assertEquals(
    studentsSortedByNameAndAge,
    students.sortedWith(compareBy({ it.name }, { it.age }))
)

也可以使用属性引用语法,代码更简洁:

assertEquals(
    studentsSortedByNameAndAge,
    students.sortedWith(compareBy(Student::name, Student::age))
)

✅ 推荐使用属性引用方式,类型安全且可读性强。

3.2 在 Comparable 中复用 Comparator

compareBy() 返回的是 Comparator<T> 实例,因此可以在 compareTo() 中直接复用:

override fun compareTo(other: Student): Int {
    return compareBy<Student>({ it.name }, { it.age }).compare(this, other)
}

这样既能保持接口一致性,又能避免重复编写比较逻辑。

3.3 支持排序方向控制

通过 compareByDescendingthenBy 系列函数,可以灵活指定每个字段的排序方向:

assertEquals(
    listOf(
        Student(name = "C", age = 9),
        Student(name = "B", age = 10, country = "C2"),
        Student(name = "A", age = 11, country = "C1"),
        Student(name = "A", age = 10),
    ),
    students.sortedWith(compareByDescending<Student> { it.name }.thenByDescending { it.age })
)

📌 解读:先按 name 降序,再按 age 降序。

支持的链式方法包括:

  • thenBy() / thenByDescending()
  • thenByOrNull() / thenByNullsFirst() / thenByNullsLast()

3.4 处理可空字段排序

可空字段(nullable)的排序需要特别注意,默认情况下 null 值会被排在最前面

assertEquals(
    listOf(
        Student(name = "A", age = 10),
        Student(name = "C", age = 9),
        Student(name = "A", age = 11, country = "C1"),
        Student(name = "B", age = 10, country = "C2"),
    ),
    students.sortedWith(compareBy<Student> { it.country }.thenBy { it.name })
)

如果你希望 null 值排在末尾,使用 nullsLast()

assertEquals(
    listOf(
        Student(name = "A", age = 11, country = "C1"),
        Student(name = "B", age = 10, country = "C2"),
        Student(name = "A", age = 10),
        Student(name = "C", age = 9),
    ),
    students.sortedWith(compareBy<Student, String?>(nullsLast()) { it.country }.thenBy { it.name })
)

反之,若想强调 null 的优先级(仍放在前面但自定义顺序),可用 nullsFirst(reverseOrder())

assertEquals(
    listOf(
        Student(name = "A", age = 10),
        Student(name = "C", age = 9),
        Student(name = "B", age = 10, country = "C2"),
        Student(name = "A", age = 11, country = "C1"),
    ),
    students.sortedWith(compareBy<Student, String?>(nullsFirst(reverseOrder())) { it.country }.thenBy { it.name })
)

⚠️ 踩坑提醒:不显式处理 null 时,其行为可能不符合直觉。建议明确使用 nullsFirstnullsLast 来提升代码可读性和健壮性。

3.5 使用自定义 Comparator 的 comparing 函数

对于复杂比较逻辑(如提供默认值、自定义比较器),推荐使用 comparing() 函数:

val defaultCountry = "C11"
assertEquals(
    listOf(
        Student(name = "A", age = 11, country = "C1"),
        Student(name = "A", age = 10),
        Student(name = "C", age = 9),
        Student(name = "B", age = 10, country = "C2"),
    ),
    students.sortedWith(
        comparing<Student?, String?>(
            { it.country },
            { c1, c2 -> (c1 ?: defaultCountry).compareTo(c2 ?: defaultCountry) }
        ).thenComparing(
            { it.age },
            { a1, a2 -> (a1 % 10).compareTo(a2 % 10) }
        )
    )
)

📌 说明:

  • 第一维度:country 字段为空时使用默认值 "C11" 参与比较
  • 第二维度:age 按个位数排序(即模 10 后比较)

✅ 这种方式适合需要精细控制比较行为的场景,扩展性强。

4. 原地排序:使用 sortWith 函数

前面的例子都基于不可变集合,sortedWith() 会返回新列表。如果操作的是可变集合(MutableList),并且希望原地修改以节省内存开销,应使用 sortWith()

val mutableStudents = students.toMutableList()
mutableStudents.sortWith(compareBy(Student::name, Student::age))
assertEquals(
    studentsSortedByNameAndAge,
    mutableStudents
)

sortWith() 直接修改原列表,适用于性能敏感或大数据量场景。

它同样支持所有 Comparator 构造方式,如 compareByDescendingnullsLastcomparing 等,用法完全一致。

5. 总结

方式 适用场景 是否创建新对象 是否支持 null 控制
实现 Comparable 有固定自然排序 ❌ 返回新列表(sorted() ❌ 不易处理
sortedWith + compareBy 通用、灵活排序 ✅ 创建新列表 ✅ 支持
sortWith 可变集合、追求性能 ❌ 原地修改 ✅ 支持
comparing + 自定义比较器 复杂逻辑(如默认值) ✅/❌ 视调用方式而定 ✅ 支持

📌 最佳实践建议:

  • ⚠️ 避免随意实现 Comparable,除非确实存在唯一的自然排序
  • ✅ 多用 compareBy().thenBy() 链式调用,清晰表达排序优先级
  • ✅ 显式处理 null 值,使用 nullsFirst / nullsLast 提高可维护性
  • ✅ 大数据量下优先考虑 sortWith 减少对象分配

所有示例代码均已上传至 GitHub:https://github.com/baeldung/kotlin-tutorials/tree/master/core-kotlin-modules/core-kotlin-collections-3


原始标题:Sort Collection of Objects by Multiple Fields in Kotlin