Avatar ·

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

Всем привет! Я только начал изучать Kotlin и столкнулся с задачей, которая кажется простой, но я никак не могу найти элегантное решение. У меня есть список объектов класса `Person`: ```kotlin data class Person( val name: String, val age: Int, val salary: Double ) ``` Мне нужно отсортировать этот список сначала по возрасту (по возрастанию), а затем, если возраст одинаковый, по имени (в алфавитном порядке), и наконец, если и возраст, и имя совпадают, по зарплате (по убыванию). В Java я бы использовал цепочку компараторов, но в Kotlin я вижу, что есть функция `sortedBy`, но она принимает только одно поле. Есть `sortedWith` и `compareBy`, но я не до конца понимаю, как их правильно комбинировать. Я пробовал такой код: ```kotlin val sortedList = list.sortedBy { it.age }.sortedBy { it.name }.sortedByDescending { it.salary } ``` Но это явно не работает, потому что каждая последующая сортировка перезаписывает предыдущую. Как правильно реализовать многоуровневую сортировку в Kotlin? Желательно без написания собственного компаратора вручную, если есть более лаконичный способ. Буду благодарен за примеры кода и объяснение. Спасибо! <<>>

Правильный подход: используйте sortedWith и compareBy

В Kotlin для сортировки по нескольким полям существует элегантное решение через функцию sortedWith в сочетании с compareBy или thenBy. Эти функции позволяют строить цепочки компараторов без лишнего кода.

Вот как решить вашу задачу:

val sortedList = list.sortedWith(
    compareBy<Person> { it.age }
        .thenBy { it.name }
        .thenByDescending { it.salary }
)

Или более короткая версия с использованием compareBy напрямую:

val sortedList = list.sortedWith(
    compareBy({ it.age }, { it.name }, { it.salary })
)

Но в вашем случае третье поле нужно сортировать по убыванию, поэтому первый вариант предпочтительнее.

Как это работает

  • compareBy — создаёт компаратор на основе переданных селекторов. Все селекторы применяются по порядку, пока не будет найдено различие.
  • thenBy — добавляет дополнительное поле для сортировки по возрастанию.
  • thenByDescending — добавляет поле для сортировки по убыванию.
  • sortedWith — принимает готовый компаратор и применяет его к коллекции.

Полный пример

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

fun main() {
    val people = listOf(
        Person("Alice", 30, 50000.0),
        Person("Bob", 25, 60000.0),
        Person("Charlie", 30, 55000.0),
        Person("Alice", 25, 70000.0)
    )

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

    sortedPeople.forEach { println(it) }
    // Вывод:
    // Person(name=Alice, age=25, salary=70000.0)
    // Person(name=Bob, age=25, salary=60000.0)
    // Person(name=Alice, age=30, salary=50000.0)
    // Person(name=Charlie, age=30, salary=55000.0)
}

Альтернативные варианты

Если вам нужно отсортировать список на месте (мутабельная коллекция), используйте sortWith:

val mutableList = people.toMutableList()
mutableList.sortWith(
    compareBy<Person> { it.age }
        .thenBy { it.name }
        .thenByDescending { it.salary }
)

Для обратной сортировки всего сразу используйте reversed() или sortedDescending(), но это редко нужно при многоуровневой сортировке.

Почему ваш первоначальный код не работал

Когда вы пишете list.sortedBy { it.age }.sortedBy { it.name }, каждая последующая функция sortedBy создаёт новую коллекцию, полностью пересортировывая её по новому полю. В результате остаётся только последняя сортировка. Это как если бы вы сначала рассортировали бумаги по дате, а потом перемешали их и рассортировали по алфавиту — порядок по дате будет потерян.

Дополнительный совет

Если типы полей сложные или вам нужно реализовать нестандартную логику сравнения, вы всегда можете создать свой компаратор через Comparator:

val customComparator = Comparator<Person> { a, b ->
    when {
        a.age != b.age -> a.age.compareTo(b.age)
        a.name != b.name -> a.name.compareTo(b.name)
        else -> b.salary.compareTo(a.salary) // по убыванию
    }
}
val sorted = list.sortedWith(customComparator)

Но для стандартных случаев compareBy и thenBy — самый чистый и читаемый способ.

<<>>

Альтернативный подход: сортировка с помощью sortedBy и тюплов

Если вам не нравится синтаксис с compareBy, можно использовать трюк с тюплами (Pair, Triple). В Kotlin тюплы сравниваются лексикографически — сначала по первому элементу, затем по второму и т.д. Это позволяет написать компактный код:

val sortedList = list.sortedWith(
    compareBy { Triple(it.age, it.name, -it.salary) }
)

Или ещё короче, если использовать только sortedBy:

val sortedList = list.sortedBy { Triple(it.age, it.name, -it.salary) }

Минус перед it.salary — это хак для имитации сортировки по убыванию (так как Double поддерживает унарный минус). Для целых чисел этот трюк тоже работает.

Плюсы и минусы этого подхода

  • Плюс: Очень лаконично, всего одна строка.
  • Минус: Неочевидный хак с минусом для обратного порядка.
  • Минус: Если нужно сортировать по возрастанию для одного поля и по убыванию для другого, приходится извращаться (например, использовать nullsFirst или отрицательные значения).
  • Минус: Для более чем трёх полей придётся использовать вложенные тюплы или свои классы, что снижает читаемость.

Этот метод хорошо подходит для простых случаев (2-3 поля, все по возрастанию). Для вашей задачи (смешанный порядок) я рекомендую первый вариант с thenByDescending — он более явный и гибкий.