1. Введение
Сортировка — это как навести порядок на столе перед тем, как искать нужную бумажку. Вроде бы можно и без этого: «я же помню, где лежит», но через неделю стол превращается в археологический слой из чеков, стикеров и проводов. В программах то же самое: данные приходят в каком-то порядке, но пользователю часто нужен другой — по возрастанию, по убыванию, «самые большие сверху», «в алфавитном порядке».
Самое приятное в Kotlin то, что сортировка выглядит как «взяли список → получили новый отсортированный список». И это ключевая идея сегодняшней лекции: мы не ломаем исходные данные (то есть не мутируем источник), а строим результат отдельно. Это звучит занудно, но на практике экономит вам часы отладки и пару нервных клеток.
Чтобы было на что опираться, будем развивать наш маленький консольный проект (условно назовём его ExpensePocket): это мини‑учёт расходов, где мы храним суммы трат и иногда хотим увидеть их «по порядку», а не «как добавляли».
2. Естественный порядок и Comparable
Когда вы просите Kotlin «просто отсортировать список», Kotlin должен понимать, как сравнивать элементы. Для чисел и строк это очевидно (ну, почти), а для каких-нибудь «сложных объектов» (которые у нас появятся сильно позже) — уже нет.
В стандартной библиотеке Kotlin сортировки sorted() и sortedDescending() работают для элементов, у которых есть естественный порядок. Формально это значит: элемент умеет сравнивать себя с другим элементом того же типа (это связано с интерфейсом Comparable). Документация Kotlin прямо описывает, что sorted() и sortedDescending() сортируют по естественному порядку и возвращают новый список, не меняя исходную коллекцию.
Для новичка это удобно воспринимать так: «если тип выглядит как то, что можно упорядочить, Kotlin уже знает как».
Небольшая табличка для ориентира:
| Тип элементов | Как выглядит «естественный порядок» | Пример результата |
|---|---|---|
|
по числовому значению | |
|
по коду символа | |
|
лексикографически (примерно «по словарю») | |
|
обычно |
|
Со строками есть тонкость: «словарный порядок» в программировании чаще всего не про «русский словарь», а про сравнение символов по кодам, поэтому "10" и "2" сравниваются как строки, а не как числа. Это не баг — это просто другой смысл данных.
3. Базовые сортировки: sorted() и sortedDescending()
sorted() и сортировка по возрастанию
sorted() — это функция, которая берёт вашу коллекцию и возвращает новый список, где элементы расположены в порядке возрастания (по естественному порядку). Важнейшая мысль: она не «перетряхивает» исходный список, а создаёт результат отдельно. В документации Kotlin это прямым текстом подчёркнуто: сортировки для read‑only коллекций возвращают результат как новую коллекцию.
Начнём с очень простого примера — числа:
fun main() {
val xs = listOf(3, 1, 2)
val sortedXs = xs.sorted()
println(xs) // [3, 1, 2]
println(sortedXs) // [1, 2, 3]
}
Здесь удобно заметить две вещи. Во-первых, xs не поменялся. Во-вторых, если вы забудете сохранить результат в переменную, то вам покажется, что сортировка «не работает» — потому что она не имеет права менять ваш исходный список (и это, на самом деле, хорошо).
Теперь строки:
fun main() {
val names = listOf("Bob", "Ann", "Charlie")
val sortedNames = names.sorted()
println(sortedNames) // [Ann, Bob, Charlie]
}
Пауза: «ничего не произошло»
Новички часто пишут так:
fun main() {
val xs = listOf(3, 1, 2)
xs.sorted()
println(xs) // [3, 1, 2]
}
Это не ошибка компиляции, и Kotlin не обязан ругаться. Вы просто попросили: «дай мне отсортированную копию», Kotlin дал (в никуда), а потом вы вывели оригинал.
Такой код — как заказать кофе и уйти из кофейни, не забрав его. Кофе существует… но уже не ваш.
sortedDescending() — сортировка по убыванию
sortedDescending() делает почти то же самое, что sorted(), только порядок становится обратным: сначала «большие», потом «маленькие». И снова: возвращается новый список, исходник не меняется. Kotlin в документации ставит sorted() и sortedDescending() рядом как базовые сортировки по естественному порядку.
Пример с числами:
fun main() {
val scores = listOf(10, 70, 50, 20)
val sorted = scores.sortedDescending()
println(scores) // [10, 70, 50, 20]
println(sorted) // [70, 50, 20, 10]
}
Пример со строками:
fun main() {
val words = listOf("one", "two", "three", "four")
println(words.sortedDescending()) // [two, three, one, four]
}
Если результат кажется неожиданным, это нормально: строки сортируются лексикографически, и иногда порядок может «не совпадать с интуицией про длину слова». Длину мы научимся использовать как критерий позже; сегодня держим фокус на естественном порядке.
4. Принцип «не мутируем источник»
Фраза «не мутировать источник» звучит как совет из книжки по чистому коду, который легко пролистать. Но на практике это страховка от очень неприятных эффектов, особенно когда в программе один и тот же список используется в разных местах.
Kotlin явно разделяет операции «верни новый результат» и операции «измени коллекцию на месте». И даже в документации про операции коллекций есть важная мысль: есть пары функций, где одна сортирует на месте (sort()), а другая возвращает новый список (sorted()). Мы сегодня сознательно тренируемся на варианте sorted(), потому что он проще для понимания и чаще приводит к более предсказуемому коду.
Представьте, что в нашем ExpensePocket у нас есть список расходов «в порядке добавления» — это полезно, потому что так видно историю: что добавляли и когда. Но иногда мы хотим увидеть «те же числа, но по возрастанию» или «по убыванию». Если бы сортировка меняла исходник, мы бы потеряли историю добавления. А с sorted() мы получаем оба варианта: история остаётся, отчёт строится отдельно.
Вот визуальная схема того, что происходит:
flowchart LR
A["Исходный список расходов (порядок добавления)"] --> B["sorted() / sortedDescending()"]
B --> C["Новый список (отсортированный)"]
A --> D["Исходный список остаётся как был"]
Видите «две ветки»? Это и есть главная идея: сортировка — это шаг обработки данных, а не переписывание памяти.
5. Сортировка в проекте ExpensePocket
Теперь сделаем сортировку не «в вакууме», а как часть маленького приложения. Пусть у нас хранится список сумм расходов (MutableList<Int>), и есть команды: добавить расход и вывести список. Мы сделаем два режима вывода: «как добавляли» и «отсортировано по убыванию».
Хранилище расходов
Сейчас нам не нужен Pair или «запись расхода» — это будет позже. Сегодня тренируемся на базовой сортировке, поэтому пусть данные будут максимально простыми.
import kotlin.system.exitProcess
fun main() {
val expenses = mutableListOf<Int>()
while (true) {
print("Команда (add/list/list-desc/exit): ")
when (readln().trim()) {
"add" -> addExpense(expenses)
"list" -> printExpenses(expenses)
"list-desc" -> printExpensesDesc(expenses)
"exit" -> exitProcess(0)
}
}
}
Обратите внимание: expenses — MutableList, то есть мы можем добавлять туда элементы. Но сортировать для отчёта мы будем через sorted()/sortedDescending(), чтобы не разрушать исходный порядок.
Добавление расхода
Сделаем маленькую функцию ввода числа. Мы уже умеем парсить Int и обрабатывать ошибки (хотя бы через toIntOrNull()), поэтому будет просто.
fun addExpense(expenses: MutableList<Int>) {
print("Сумма расхода: ")
val value = readln().trim().toIntOrNull()
if (value == null) {
println("Это не число, расход не добавлен.")
return
}
expenses.add(value)
println("Добавлено: $value")
}
Печать «как добавляли»
Это наш «исторический» режим: показываем список как есть.
fun printExpenses(expenses: List<Int>) {
println("Расходы (как добавляли): $expenses")
}
Печать «по убыванию»
Вот тут и появляется наша тема. Мы строим отсортированную копию и выводим её. Исходный список не трогаем.
fun printExpensesDesc(expenses: List<Int>) {
val sorted = expenses.sortedDescending()
println("Расходы (по убыванию): $sorted")
}
Функция получилась короткой, но в ней много смысла: сортировка — это не команда «переделай данные», а команда «построй мне вид отсортированных данных».
Чтобы убедиться, что мы не ломаем источник, можно временно добавить отладочный вывод:
fun printExpensesDesc(expenses: List<Int>) {
val sorted = expenses.sortedDescending()
println("Оригинал: $expenses")
println("Сортировка: $sorted")
}
Если вы добавите расходы 10, 5, 20, то увидите примерно такое:
Оригинал: [10, 5, 20]
Сортировка: [20, 10, 5]
И это именно то поведение, которое мы хотим в адекватной программе.
6. Почему иногда кажется, что сортировка меняет список
Иногда возникает странное ощущение: «Я же ничего не сортировал “внутри”, но почему в другом месте программы данные стали другими?» Это обычно происходит не из-за sorted(), а из-за того, что вы где-то случайно начали использовать один и тот же изменяемый список под двумя именами (aliasing). Это как раз связано с тем, что мы обсуждали раньше в теме «значение vs ссылка».
С sorted() таких сюрпризов меньше, потому что она возвращает новый список, а не «представление того же самого списка». Kotlin в документации прямо подчёркивает, что sorted() создаёт новую коллекцию (в отличие от операций, которые меняют mutable-коллекции на месте).
Но вы можете создать сюрприз сами, если сделаете так:
fun main() {
val a = mutableListOf(3, 1, 2)
val b = a
b.add(10)
println(a) // [3, 1, 2, 10]
}
Это не имеет отношения к сортировке — это про «две переменные, один список». Поэтому принцип «не мутируем источник» особенно хорош, когда у вас программа становится больше: вы меньше зависите от того, кто и где случайно поменял общие данные.
Списки и равенство: порядок — часть содержимого
Когда вы сортируете, вы меняете порядок элементов. И даже если набор значений «тот же», порядок считается частью содержимого списка. Это важно, потому что потом вы можете сравнивать списки через == и удивляться.
Посмотрите:
fun main() {
val a = listOf(1, 2, 3)
val b = listOf(3, 2, 1)
println(a == b) // false
}
Для человека это может быть «одни и те же числа». Для списка это «другая последовательность». И это логично: список — упорядоченная коллекция.
Эта мысль пригодится, когда вы будете делать проверки «что-то изменилось или нет» и сравнивать результаты операций.
7. Типичные ошибки при работе с sorted() и sortedDescending()
Ошибка №1: вызывать sorted(), но не использовать результат.
Это самый частый случай: написали xs.sorted() и ждёте, что xs станет отсортированным. Но sorted() возвращает новый список и не меняет исходный — это базовый контракт сортировок для read‑only коллекций. Исправление простое: либо сохранить в val sorted = xs.sorted(), либо сразу вывести println(xs.sorted()).
Ошибка №2: считать, что сортировка “обязана” работать для любого типа элементов.
sorted() и sortedDescending() рассчитаны на элементы, у которых есть естественный порядок, то есть они сравнимы. Если вы попытаетесь сортировать что-то, что Kotlin не умеет сравнивать “само по себе”, вы упрётесь в ограничения типов. На текущем этапе курса это нормальный сигнал: значит, пока сортируем числа и строки, а «как сортировать более сложные штуки» — отдельная тема.
Ошибка №3: путать сортировку и разворот.
Иногда кажется, что sortedDescending() — это «отсортировать и перевернуть», и люди начинают пытаться делать странные комбинации вроде «я сначала отсортировал, потом ещё раз отсортировал». Смысл проще: sorted() — по возрастанию, sortedDescending() — по убыванию. Если вам просто нужно показать элементы в обратном порядке добавления, это вообще другая задача (и другая операция), и она не про сортировку по значению.
Ошибка №4: использовать сортировку там, где нужна логика “самый большой/самый маленький”, и делать лишнюю работу.
Очень частая неэффективность у новичков выглядит так: «хочу максимальный элемент» → «отсортирую весь список» → «возьму первый». Работать будет, но программа делает больше, чем нужно. На данном этапе мы это не оптимизируем (нам важнее понятность), но полезно заметить сам факт: сортировка — тяжёлый инструмент, и его стоит применять осознанно.
Ошибка №5: терять “порядок добавления” в исходном списке, потому что вы где-то всё-таки изменили его на месте.
Даже если вы сегодня используете sorted() правильно, где-то рядом может появиться другая операция, которая меняет MutableList. Kotlin прямо различает «создать новый результат» и «изменить состояние коллекции», и это различие важно держать в голове. Хорошая привычка на старте — строить отчёты через sorted()/sortedDescending(), а исходные данные трогать только там, где вы явно добавляете/удаляете элементы.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ