Сортировка коллекций

Для некоторых типов коллекций является важным аспектом то, как упорядочены их элементы. Например, два списка с одинаковыми элементами не равны, если их элементы упорядочены по-разному.

В Kotlin порядок объектов можно задать несколькими способами.

Во-первых, существует естественный порядок (ориг. natural order). Он характерен для наследников интерфейса Comparable. Если не был указан другой порядок сортировки, то они будут отсортированы в естественном порядке.

Большинство встроенных типов являются Comparable:

  • Числовые типы используют традиционный числовой порядок: 1 больше, чем 0; -3.4f больше, чем -5f и т.д.
  • Char и String используют лексикографический порядок: b больше, чем a; world больше, чем hello.

Таким образом, чтобы ваш собственный класс при сортировке использовал естественный порядок, сделайте его наследником Comparable. Дополнительно потребуется реализовать функцию compareTo(). В качестве аргумента она должна принимать другой объект такого же типа, а возвращать целое число, указывающее, какой из объектов больше:

  • Положительное значение показывает, что объект-приёмник больше аргумента.
  • Отрицательное значение показывает, что объект-приёмник меньше аргумента.
  • Ноль показывает, что объекты равны.

В примере ниже показан класс для сортировки версий, состоящий из переменных major и minor.

class Version(val major: Int, val minor: Int): Comparable<Version> {
    override fun compareTo(other: Version): Int {
        if (this.major != other.major) {
            return this.major - other.major
        } else if (this.minor != other.minor) {
            return this.minor - other.minor
        } else return 0
    }
}

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

Пользовательский порядок (ориг. custom order) позволяет вам сортировать экземпляры любого типа так, как вам нравится. В частности, вы можете определить порядок для сортировки объектов, не являющихся наследниками Comparable, а также задать Comparable типам порядок, отличный от естественного. Осуществляется это при помощи Comparator. У Comparator есть функция compare(), которая принимает два объекта, а в качестве результата сравнения возвращает целое число. Результат интерпретируется так же, как описано выше для функции compareTo().

fun main() {
    val lengthComparator = Comparator { str1: String, str2: String -> str1.length - str2.length }
    println(listOf("aaa", "bb", "c").sortedWith(lengthComparator)) // [c, bb, aaa]
}

С помощью lengthComparator вы можете изменить порядок сортировки строк: вместо сортировки в лексикографическом порядке они будут отсортированы по длине.

Существует более упрощённый вариант использования Comparator - функция compareBy() из стандартной библиотеки. Она принимает лямбда-функцию, которая из объекта создаёт Comparable значение и задаёт пользовательский порядок, который по факту представляет собой естественный порядок созданных Comparable значений.

Используя compareBy() можно модифицировать lengthComparator из примера выше следующим образом:

fun main() {  
    println(listOf("aaa", "bb", "c").sortedWith(compareBy { it.length })) // [c, bb, aaa]
}

Kotlin предоставляет функции, которые позволяют сортировать коллекции в естественном, пользовательском и даже в случайном порядке. В текущем разделе рассмотрены функции сортировки, которые работают только с неизменяемыми коллекциями. Такие функции возвращают результат сортировки в виде новой коллекции, содержащей элементы исходной коллекции, но упорядоченные в соответствии с заданным условием. Информация о функциях для сортировки изменяемых коллекций находится в разделе List: специфичные операции.

Естественный порядок

Основные функции sorted() и sortedDescending() возвращают элементы коллекции, отсортированные по возрастанию или убыванию в соответствии с их естественным порядком. Эти функции могут быть применены к коллекциям, содержащим элементы Comparable типа.

fun main() {
    val numbers = listOf("one", "two", "three", "four")

    println("Sorted ascending: ${numbers.sorted()}") // Sorted ascending: [four, one, three, two]
    println("Sorted descending: ${numbers.sortedDescending()}") // Sorted descending: [two, three, one, four]
}

Пользовательский порядок

Для сортировки объектов в пользовательском порядке или для сортировки объектов, не являющихся наследниками Comparable, существуют функции sortedBy() и sortedByDescending(). Они используют функцию-селектор для преобразования элементов коллекции в Comparable значения и сортируют коллекцию в естественном порядке этих значений.

fun main() {
    val numbers = listOf("one", "two", "three", "four")

    val sortedNumbers = numbers.sortedBy { it.length }
    println("Sorted by length ascending: $sortedNumbers") // Sorted by length ascending: [one, two, four, three]
    val sortedByLast = numbers.sortedByDescending { it.last() }
    println("Sorted by the last letter descending: $sortedByLast") // Sorted by the last letter descending: [four, two, one, three]
}

Чтобы задать пользовательский порядок для сортировки коллекции, вы можете предоставить свой собственный Comparator. Для этого вызовите функцию sortedWith() и передайте ей свой Comparator. С помощью этой функции сортировка строк по их длине будет выглядеть следующим образом:

fun main() {
    val numbers = listOf("one", "two", "three", "four")
    println("Sorted by length ascending: ${numbers.sortedWith(compareBy { it.length })}") // Sorted by length ascending: [one, two, four, three]
}

Обратный порядок

С помощью функции reversed() вы можете получить коллекцию, отсортированную в обратном порядке.

fun main() {
    val numbers = listOf("one", "two", "three", "four")
    println(numbers.reversed()) // [four, three, two, one]
}

reversed() возвращает новую коллекцию с копиями элементов из исходной коллекции. Поэтому, если вы измените исходную коллекцию, это не повлияет на ранее полученный от reversed() результат.

Другая функция - asReversed() - возвращает перевёрнутое представление исходной коллекции. Эта функция может быть более легковесной и предпочтительной, чем reversed(), при условии, что исходная коллекция не будет изменяться.

fun main() {
    val numbers = listOf("one", "two", "three", "four")
    val reversedNumbers = numbers.asReversed()
    println(reversedNumbers) // [four, three, two, one]
}

Если же в исходную коллекцию будут внесены изменения, все эти изменения отразятся в её перевёрнутых представлениях и наоборот.

fun main() {
    val numbers = mutableListOf("one", "two", "three", "four")
    val reversedNumbers = numbers.asReversed()
    println(reversedNumbers) // [four, three, two, one]
    numbers.add("five")
    println(reversedNumbers) // [five, four, three, two, one]
}

Однако, если источник не является списком, либо неизвестно является ли список изменяемым, тогда функция reversed() более предпочтительна для использования, поскольку она возвращает копию, которая в будущем не изменится.

Случайный порядок

Наконец, существует функция shuffled(), которая возвращает новый List, содержащий элементы исходной коллекции в случайном порядке. Её можно вызывать либо без аргументов, либо с объектом Random.

fun main() {
     val numbers = listOf("one", "two", "three", "four")
     println(numbers.shuffled()) // [three, four, one, two]
}