Avatar ·

Сортировка коллекции по нескольким полям в Kotlin

Всем привет! Я только начал изучать Kotlin и столкнулся с задачей сортировки списка объектов по нескольким полям. Например, у меня есть класс Person: ```kotlin data class Person(val name: String, val age: Int, val height: Double) ``` И список людей: ```kotlin val people = listOf( Person("Alice", 25, 165.0), Person("Bob", 30, 180.0), Person("Charlie", 25, 170.0), Person("Alice", 30, 160.0) ) ``` Мне нужно отсортировать этот список сначала по имени (по возрастанию), затем по возрасту (по возрастанию), а потом по росту (по убыванию). В Java я бы использовал Comparator с цепочкой thenComparing. В Kotlin есть функция sortedWith, но я не понимаю, как правильно её применить для нескольких полей. Пробовал писать что-то вроде: ```kotlin val sorted = people.sortedWith(compareBy({ it.name }, { it.age })) ``` Но это сортирует только по двум полям. А как добавить сортировку по убыванию для роста? И вообще, есть ли более лаконичный способ? Буду благодарен за примеры с объяснением! <<>>

Решение через compareBy и thenBy

В Kotlin для сортировки коллекций по нескольким полям лучше всего использовать функции compareBy и thenBy (или thenByDescending для убывающего порядка). Они создают композитный компаратор, который можно передать в sortedWith.

Вот как решить вашу задачу — сортировка по имени (возрастание), возрасту (возрастание) и росту (убывание):

val sorted = people.sortedWith(
    compareBy<Person> { it.name }
        .thenBy { it.age }
        .thenByDescending { it.height }
)

Объяснение:

  • compareBy<Person> { it.name } — создаёт первичный компаратор по имени (по умолчанию по возрастанию).
  • .thenBy { it.age } — добавляет вторичную сортировку по возрасту (если имена равны).
  • .thenByDescending { it.height } — добавляет третичную сортировку по росту, но в обратном порядке.

Результат после сортировки:

Person(name=Alice, age=25, height=165.0)
Person(name=Alice, age=30, height=160.0)
Person(name=Bob, age=30, height=180.0)
Person(name=Charlie, age=25, height=170.0)

Обратите внимание: сначала идёт Alice 25 лет (рост 165), затем Alice 30 лет (рост 160) — это потому что возраст сортируется по возрастанию, а рост по убыванию внутри каждой группы с одинаковым именем и возрастом.

Альтернативный синтаксис с лямбдой

Можно использовать более короткую запись без явного указания типа:

val sorted = people.sortedWith(
    compareBy({ it.name }, { it.age }, { -it.height.toInt() })
)

Но этот способ менее гибкий: для убывания приходится использовать трюк с отрицанием числа, что не работает со строками или nullable-полями. Лучше придерживаться цепочки thenBy.

Сортировка с nullable-полями

Если поле может быть null, используйте thenBy(nullsLast()) или thenBy(nullsFirst()):

data class Person(val name: String?, val age: Int)

val sorted = people.sortedWith(
    compareBy<Person> { it.name }
        .thenBy(nullsLast()) { it.age }
)

Сортировка в обратном порядке всей коллекции

Если нужно полностью перевернуть результат, просто добавьте .reversed() в конце:

val sortedDesc = people.sortedWith(
    compareBy { it.name }.thenBy { it.age }
).reversed()

Производительность

Все эти методы работают за O(n log n) и используют стабильную сортировку (порядок равных элементов сохраняется). Для больших коллекций (более 10 000 элементов) можно рассмотреть использование sortedBy с цепочкой, но sortedWith обычно предпочтительнее из-за читаемости.

В итоге, ваш код будет выглядеть так:

val sortedPeople = people.sortedWith(
    compareBy<Person> { it.name }
        .thenBy { it.age }
        .thenByDescending { it.height }
)
println(sortedPeople)
<<>>

Использование Kotlin Comparator напрямую

Если вам не нравится синтаксис с цепочкой, можно создать компаратор вручную. Это даёт больше контроля и может быть полезно в сложных случаях, например, когда логика сортировки зависит от внешних условий.

val comparator = Comparator<Person> { a, b ->
    val nameCompare = a.name.compareTo(b.name)
    if (nameCompare != 0) return@Comparator nameCompare
    
    val ageCompare = a.age.compareTo(b.age)
    if (ageCompare != 0) return@Comparator ageCompare
    
    b.height.compareTo(a.height) // убывание — меняем a и b местами
}

val sorted = people.sortedWith(comparator)

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

Расширение для списка

Вы также можете написать функцию-расширение для многокритериальной сортировки:

fun <T> List<T>.sortedByMultiple(vararg selectors: Pair<(T) -> Comparable<*>, Boolean>): List<T> {
    return this.sortedWith { a, b ->
        for ((selector, ascending) in selectors) {
            val compareResult = if (ascending) {
                compareValuesBy(a, b, selector)
            } else {
                compareValuesBy(b, a, selector)
            }
            if (compareResult != 0) return@sortedWith compareResult
        }
        0
    }
}

// Использование:
val sorted = people.sortedByMultiple(
    { it.name } to true,
    { it.age } to true,
    { it.height } to false
)

Этот вариант лаконичен и легко читается, но требует написания дополнительного кода. Выбирайте тот подход, который лучше подходит под ваш стиль и требования проекта.