Почему «простая» сортировка в Kotlin ломает код?
Привет, коллега! Если ты когда-нибудь писал на Kotlin и пытался отсортировать список задач сначала по дате, а потом по приоритету, то наверняка сталкивался с ситуацией, когда результат выглядит как случайный набор. Ты не один — вопрос «Sort collection by multiple fields in Kotlin» собрал тысячи просмотров на Stack Overflow. Казалось бы, что сложного? Но дьявол, как всегда, в деталях.
В этой статье мы разберём топ ошибок при сортировке по нескольким полям в Kotlin, которые допускают даже опытные разработчики. Ты узнаешь, как работает sortedWith, почему compareBy и thenBy — твои лучшие друзья, и как не потерять данные из-за null. Поехали!
Ошибка №1: Сортировка как в Java — через Comparator вручную
Kotlin — это не Java. Но многие, приходя из мира Java, пишут:
val list = listOf(
User("Alice", 25),
User("Bob", 20),
User("Charlie", 25)
)
val sorted = list.sortedWith(Comparator { a, b ->
val ageCompare = a.age.compareTo(b.age)
if (ageCompare != 0) ageCompare else a.name.compareTo(b.name)
})
Да, это работает. Но это ужасно читаемо, и ты легко запутаешься в цепочках if-else. Kotlin предлагает элегантный способ — функции-компараторы.
Как правильно?
val sorted = list.sortedWith(compareBy<User> { it.age }.thenBy { it.name })
Коротко, ясно, без лишней логики. compareBy создаёт первичный компаратор, а thenBy добавляет вторичные поля. Это и есть сортировка по нескольким полям в Kotlin в её идеальном виде.
Ошибка №2: Игнорирование null при сортировке
Допустим, у нас есть список задач, где deadline может быть null. Если ты просто напишешь:
tasks.sortedBy { it.deadline }
…то получишь NullPointerException. Kotlin строг к null, и это прекрасно. Но как быть, если null — это нормально?
Решение: nullsFirst / nullsLast
Используй специальные обёртки:
val sorted = tasks.sortedWith(
compareBy<Task>(nullsLast()) { it.deadline }
.thenBy { it.priority }
)
Здесь nullsLast() помещает все null-даты в конец списка. Есть и nullsFirst() — наоборот. Это решает проблему, и твоя сортировка коллекций Kotlin становится безопасной.
Ошибка №3: Путаница между sortedBy и sortedWith
Многие новички пишут:
list.sortedBy { it.age }.sortedBy { it.name }
И удивляются, что сортировка по имени «перебивает» сортировку по возрасту. sortedBy каждый раз создаёт новый список и сортирует только по одному полю. Последний вызов — главный.
Как исправить?
Используй sortedWith с цепочкой compareBy:
list.sortedWith(compareBy({ it.age }, { it.name }))
Или многострочный вариант для сложной логики:
list.sortedWith(compareBy<Person> { it.age }.thenByDescending { it.name })
Запомни: один вызов sortedWith — одна сортировка по нескольким полям.
Ошибка №4: Сортировка в обратном порядке — не там, где надо
Иногда нужно отсортировать по возрастанию возраста, но по убыванию имени. Типичная ошибка:
list.sortedByDescending { it.age }.sortedBy { it.name }
Опять та же проблема — вторая сортировка ломает первую. Правильный подход:
val sorted = list.sortedWith(
compareBy<Person> { it.age }
.thenByDescending { it.name }
)
Или для полного контроля:
val sorted = list.sortedWith(
compareBy(Person::age).thenByDescending(Person::name)
)
Обрати внимание: thenByDescending работает только внутри цепочки compareBy. Всё просто и логично.
Ошибка №5: Сортировка mutable-списков без учёта производительности
Для MutableList есть метод sortWith, который сортирует на месте. Но если ты напишешь:
val mutableList = mutableListOf(...)
mutableList.sortWith(compareBy { it.field1 }.thenBy { it.field2 })
…то всё будет ок. Но ошибка в том, что некоторые используют sortedWith на mutable-списке, создавая новый объект, а потом удивляются, почему исходный не изменился.
Совет
- Если нужен новый список — используй
sortedWith(возвращаетList). - Если нужно изменить текущий — используй
sortWith(работает сMutableList).
Это сэкономит память и время. Сортировка коллекций Kotlin должна быть осознанной.
Продвинутый приём: кастомный компаратор для сложных случаев
Бывает, что стандартных функций недостаточно. Например, нужно отсортировать по длине строки, а потом по алфавиту. Тут пригодится кастомный Comparator:
val customComparator = Comparator { a, b ->
val byLength = a.name.length.compareTo(b.name.length)
if (byLength != 0) byLength else a.name.compareTo(b.name)
}
val sorted = list.sortedWith(customComparator)
Но не злоупотребляй — для 90% случаев хватает compareBy и thenBy.
Как не допустить ошибок? Чек-лист
Вот краткий список того, что нужно помнить при сортировке по нескольким полям в Kotlin:
- Всегда используй
sortedWith+compareByдля множественной сортировки. - Не вызывай
sortedByнесколько раз подряд — последняя сортировка отменяет предыдущие. - Обрабатывай null с помощью
nullsFirst()илиnullsLast(). - Для обратного порядка используй
thenByDescendingвнутри цепочки. - Выбирай
sortedWith(новый список) илиsortWith(изменение исходного) в зависимости от задачи.