Сортировка коллекции по нескольким полям в 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 — он более явный и гибкий.