9.1.1.4 排序

原文链接: https://kotlinlang.org/docs/collection-ordering.html

9.1.1.4 排序

元素的顺序是某些集合类型的重要方面。例如,包含相同元素但顺序不同的两个列表并不相等。

在 Kotlin 中,可以用多种方式定义对象的顺序。

首先是自然顺序。它是为 Comparable 接口的实现定义的。当没有指定其他顺序时,就用自然顺序对它们排序。

大多数内置类型都是可比较的:

  • 数值类型使用传统的数值顺序:1 大于 0;-3.4f 大于 -5f,依此类推。
  • Char 和 String 使用字典序:b 大于 a;world 大于 hello。

要为自定义类型定义自然顺序,请让该类型实现 Comparable。这需要实现 compareTo() 函数。compareTo() 必须接收集合同样类型的另一个对象作为实参,并返回一个表示哪个对象更大的整数值:

  • 正值表示接收者对象更大。
  • 负值表示它小于该实参。
  • 零表示两者相等。

下面是一个用于对由主版本号和次版本号组成的版本进行比较的类。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Version(val major: Int, val minor: Int): Comparable<Version> {
    override fun compareTo(other: Version): Int = when {
        this.major != other.major -> this.major compareTo other.major // 中缀形式的 compareTo()
        this.minor != other.minor -> this.minor compareTo other.minor
        else -> 0
    }
}

fun main() {
    println(Version(1, 2) > Version(1, 3))
    println(Version(2, 0) > Version(1, 5))
}

自定义顺序让你可以按自己喜欢的方式对任意类型的实例排序。特别是,你可以为非可比较对象定义顺序,或者为可比较类型定义不同于自然顺序的顺序。要为某个类型定义自定义顺序,请为它创建 Comparator。Comparator 包含 compare() 函数:它接收某个类的两个实例,并返回两者比较的整数结果。结果的解释方式与上面描述的 compareTo() 结果相同。

1
2
3
4
fun main() {
    val lengthComparator = Comparator { str1: String, str2: String -> str1.length - str2.length }
    println(listOf("aaa", "bb", "c").sortedWith(lengthComparator))
}

有了 lengthComparator,你就能按字符串长度而不是默认的字典序来排列它们。

定义 Comparator 的更简短方式是使用标准库中的 compareBy() 函数。compareBy() 接收一个从实例产生 Comparable 值的 lambda 函数,并把自定义顺序定义为这些值的自然顺序。

使用 compareBy(),上面例子中的长度比较器可以写成这样:

1
2
3
fun main() {
    println(listOf("aaa", "bb", "c").sortedWith(compareBy { it.length }))
}

你也可以基于多个条件定义顺序。例如,要按字符串长度排序、长度相同时按字母顺序排序,你可以这样写:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
fun main() {
    val sortedStrings = listOf("aaa", "bb", "c", "b", "a", "aa", "ccc")
        .sortedWith { a, b ->
           when (val compareLengths = a.length.compareTo(b.length)) {
             0 -> a.compareTo(b)
             else -> compareLengths
           }
         }

    println(sortedStrings)
    // [a, b, c, aa, bb, aaa, ccc]
}

由于按多个条件排序是常见场景,Kotlin 标准库提供了 .thenBy() 函数,用于添加次级排序规则。

例如,你可以把 compareBy() 与 .thenBy() 结合,先按长度、再按字母顺序对字符串排序,与上一个例子完全一样:

1
2
3
4
5
6
7
fun main() {
    val sortedStrings = listOf("aaa", "bb", "c", "b", "a", "aa", "ccc")
        .sortedWith(compareBy<String> { it.length }.thenBy { it })

    println(sortedStrings)
    // [a, b, c, aa, bb, aaa, ccc]
}

Kotlin 集合包提供了按自然顺序、自定义顺序甚至随机顺序对集合排序的函数。本页将介绍适用于只读集合的排序函数。这些函数会把结果作为新集合返回,其中包含按所需顺序排列的原集合元素。要了解用于就地排序可变集合的函数,请参阅 List 特有操作。

自然顺序

基本函数 .sorted() 和 .sortedDescending() 按自然顺序把集合元素以升序和降序返回。这些函数适用于由 Comparable 元素组成的集合。

1
2
3
4
5
6
fun main() {
    val numbers = listOf("one", "two", "three", "four")

    println("Sorted ascending: ${numbers.sorted()}")
    println("Sorted descending: ${numbers.sortedDescending()}")
}

自定义顺序

要按自定义顺序排序或对不可比较的对象排序,可以使用 .sortedBy() 和 .sortedByDescending() 函数。它们接收一个选择器函数,把集合元素映射为 Comparable 值,并按这些值的自然顺序对集合排序。

1
2
3
4
5
6
7
8
fun main() {
    val numbers = listOf("one", "two", "three", "four")

    val sortedNumbers = numbers.sortedBy { it.length }
    println("Sorted by length ascending: $sortedNumbers")
    val sortedByLast = numbers.sortedByDescending { it.last() }
    println("Sorted by the last letter descending: $sortedByLast")
}

要为集合排序定义自定义顺序,你可以提供自己的 Comparator。为此,请调用 .sortedWith() 扩展函数并传入你的 Comparator。用这个函数按长度对字符串排序看起来是这样的:

1
2
3
4
fun main() {
    val numbers = listOf("one", "two", "three", "four")
    println("Sorted by length ascending: ${numbers.sortedWith(compareBy { it.length })}")
}

检查排序顺序

你可以使用以下扩展函数检查元素是否已经符合指定顺序:

  • .isSorted()
  • .isSortedDescending()
  • .isSortedWith(comparator)
  • .isSortedBy(selector)
  • .isSortedByDescending(selector)

如果元素符合指定顺序,或者元素少于两个,这些扩展函数返回 true。一旦发现不满足顺序的一对元素,它们就返回 false 并停止检查。

对于迭代顺序没有保证的集合(例如 HashSet),结果可能在不同调用之间变化。对于不以一致顺序产生元素的序列也是如此。要在多次调用中得到相同结果,请只在迭代顺序有保证的集合(例如 List)上使用这些函数。

在检查 Double 和 Float 值时,这些函数把 NaN 视为大于任何其他值,把 -0.0 视为小于 0.0。此外,.isSortedBy() 和 .isSortedByDescending() 函数把选择器结果为 null 的情况视为小于任何非空值。

在序列上调用这些函数时,该操作是终止操作。它会消费该序列以产生一个 Boolean 值,而不是返回另一个序列。

注意: 这些排序顺序检查函数也可用于数组、基本类型数组和无符号数组。无符号数组及其操作是实验性的,需要使用 @ExperimentalUnsignedTypes 注解选择启用。

下面是使用 .isSorted() 和 .isSortedBy() 函数检查排序顺序的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
data class User(val name: String, val age: Int)

fun main() {
    val numbers = listOf(1, 2, 3, 4)
    println(numbers.isSorted())
    // true

    val users = listOf(
        User("Alice", 24),
        User("Bob", 31),
        User("Charlie", 29),
    )
    println(users.isSortedBy(User::age))
    // false

    val descending = listOf(4, 3, 2, 1)
    println(descending.isSortedDescending())
    // true

}

逆序

你可以使用 .reversed() 函数以逆序获取集合。

1
2
3
4
fun main() {
    val numbers = listOf("one", "two", "three", "four")
    println(numbers.reversed())
}

.reversed() 扩展函数返回一个包含元素副本的新集合。因此,如果你之后修改原集合,不会影响此前获得的 .reversed() 结果。

另一个逆序函数——.asReversed()

  • 返回同一个集合实例的逆序视图,因此如果原列表不会发生变化,它可能比 .reversed() 更轻量、更可取。
1
2
3
4
5
fun main() {
    val numbers = listOf("one", "two", "three", "four")
    val reversedNumbers = numbers.asReversed()
    println(reversedNumbers)
}

如果原列表是可变的,它的所有改动都会反映到其逆序视图中,反之亦然。

1
2
3
4
5
6
7
fun main() {
    val numbers = mutableListOf("one", "two", "three", "four")
    val reversedNumbers = numbers.asReversed()
    println(reversedNumbers)
    numbers.add("five")
    println(reversedNumbers)
}

不过,如果列表的可变性未知,或者源根本不是列表,那么 .reversed() 更可取,因为它的结果是一份将来不会改变的副本。

随机顺序

最后,还有一个函数返回包含集合元素、顺序随机的新 List——.shuffled()。你可以在不带参数的情况下调用它,也可以传入一个 Random 对象。

1
2
3
4
fun main() {
     val numbers = listOf("one", "two", "three", "four")
     println(numbers.shuffled())
}