JavaRush /Курси /Kotlin SELF /Базові сортування списків — sorted() / sortedDescending()...

Базові сортування списків — sorted() / sortedDescending()

Kotlin SELF
Рівень 23 , Лекція 1
Відкрита

1. Вступ

Сортування — це як навести лад на столі перед тим, як шукати потрібний папірець. Начебто можна й без цього: «я ж памʼятаю, де лежить». Але минає тиждень — і стіл перетворюється на археологічний шар із чеків, стікерів і дротів. У програмах те саме: дані приходять у якомусь порядку, а користувачеві часто потрібен інший — за зростанням, за спаданням, «найбільші зверху», «в алфавітному порядку».

Найприємніше в Kotlin те, що сортування виглядає так: «взяли список → отримали новий відсортований список». І це ключова ідея сьогоднішньої лекції: ми не змінюємо вихідні дані (тобто не мутуємо джерело), а будуємо результат окремо. Звучить занудно, зате на практиці економить години налагодження й кілька нервових клітин.

Щоб було на що спиратися, розвиватимемо наш маленький консольний проєкт (умовно назвімо його ExpensePocket): це мініоблік витрат, де ми зберігаємо суми й іноді хочемо побачити їх «у порядку», а не «як додавали».

2. Природний порядок і Comparable

Коли ви просите Kotlin «просто відсортувати список», йому потрібно розуміти, як порівнювати елементи. Для чисел і рядків це очевидно (ну, майже), а для «складних обʼєктів» (які в нас зʼявляться значно пізніше) — уже ні.

У стандартній бібліотеці Kotlin функції sorted() і sortedDescending() працюють для елементів, у яких є природний порядок. Формально це означає, що елемент уміє порівнювати себе з іншим елементом того самого типу (це повʼязано з інтерфейсом Comparable). Документація Kotlin прямо говорить: sorted() і sortedDescending() сортують за природним порядком і повертають новий список, не змінюючи вихідну колекцію.

Новачкові це зручно сприймати так: «якщо тип схожий на те, що можна впорядкувати, Kotlin уже знає як».

Невеличка табличка для орієнтира:

Тип елементів Як виглядає «природний порядок» Приклад результату
Int, Long, Double
за числовим значенням
1 < 2 < 10
Char
за кодом символу
'A' < 'B' < 'a'
String
лексикографічно (приблизно «за словником»)
"Ann" < "Bob" < "Charlie"
Boolean
зазвичай
false < true
false, потім true

Із рядками є тонкість: «словниковий порядок» у програмуванні найчастіше означає порівняння символів за їхніми кодами. Тому "10" і "2" порівнюються як рядки, а не як числа. Це не баг — просто інший зміст даних.

3. Базові сортування: sorted() і sortedDescending()

sorted() і сортування за зростанням

sorted() — це функція, яка бере вашу колекцію й повертає новий список, де елементи розташовано в порядку зростання (за природним порядком). Найважливіша думка: вона не «перетрушує» вихідний список, а створює результат окремо. У документації Kotlin це прямо підкреслено: сортування для колекцій лише для читання повертає результат як нову колекцію.

Почнімо з дуже простого прикладу — числа:

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)
        }
    }
}

Зверніть увагу: expensesMutableList, тобто ми можемо додавати туди елементи. Але сортувати для звіту будемо через 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() повертає новий список і не змінює вихідний — це базовий контракт сортувань для колекцій лише для читання. Виправлення просте: або зберегти в val sorted = xs.sorted(), або одразу вивести println(xs.sorted()).

Помилка №2: вважати, що сортування “зобовʼязане” працювати для будь-якого типу елементів.
sorted() і sortedDescending() розраховані на елементи з природним порядком, тобто порівнювані. Якщо ви спробуєте сортувати щось, що Kotlin не вміє порівнювати «саме по собі», ви вперетеся в обмеження типів. На поточному етапі курсу це нормальний сигнал: отже, поки сортуємо числа й рядки, а «як сортувати складніші штуки» — окрема тема.

Помилка №3: плутати сортування й розворот.
Іноді здається, що sortedDescending() — це «відсортувати й перевернути», і люди починають складати дивні комбінації на кшталт «я спочатку відсортував, потім ще раз відсортував». Сенс простіший: sorted() — за зростанням, sortedDescending() — за спаданням. Якщо вам потрібно просто показати елементи у зворотному порядку додавання, це взагалі інша задача (і інша операція). Вона не про сортування за значенням.

Помилка №4: застосовувати сортування там, де потрібна логіка “найбільший/найменший”, і робити зайву роботу.
Типова неефективність у новачків виглядає так: «хочу максимальний елемент» → «відсортую весь список» → «візьму перший». Працюватиме, але програма робить більше, ніж потрібно. На цьому етапі ми це не оптимізуємо (нам важливіша зрозумілість), але корисно помітити сам факт: сортування — «важкий» інструмент, і його варто застосовувати усвідомлено.

Помилка №5: втрачати “порядок додавання” у вихідному списку, бо ви десь усе-таки змінили його на місці.
Навіть якщо сьогодні ви правильно використовуєте sorted(), десь поруч може зʼявитися інша операція, яка змінює MutableList. Kotlin прямо розрізняє «створити новий результат» і «змінити стан колекції», і цю різницю важливо тримати в голові. Хороша звичка на старті — будувати звіти через sorted()/sortedDescending(), а вихідні дані чіпати лише там, де ви явно додаєте або видаляєте елементи.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ