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 使用 sortedWith 与 compareBy
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 支持排序方向控制
通过 compareByDescending
和 thenBy
系列函数,可以灵活指定每个字段的排序方向:
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 时,其行为可能不符合直觉。建议明确使用 nullsFirst
或 nullsLast
来提升代码可读性和健壮性。
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
构造方式,如 compareByDescending
、nullsLast
、comparing
等,用法完全一致。
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