1. Map обходиться не як список: ключі замість індексів
Якщо List схожий на пронумерований список справ, то Map — це радше телефонна книга: за «ім’ям» (ключем) ми шукаємо «номер» (значення). Тому спроба «пройтися мапою за індексами» звучить так само дивно, як прохання «дай мені третій контакт за рахунком», не уточнюючи, за яким саме порядком. У Kotlin Map<K, V> зберігає пари «ключ → значення». Ключі в ній унікальні, а порядок обходу не має бути основою логіки.
Найважливіша думка: Map — це не «список пар», а структура, у якій доступ до даних відбувається через ключ. Саме з цієї ідеї й випливають правильні способи обходу.
Нагадування: як створюється Map і що таке Pair
Перш ніж обходити, треба мати що обходити. Зазвичай мапу створюють через mapOf(...) або mutableMapOf(...), передаючи туди пари Pair<K, V>. Найчастіше пари створюють через інфікс-функцію to, яка виглядає як маленька стрілочка — «ключ до значення».
Наприклад:
fun main() {
val prices = mapOf(
"coffee" to 250,
"tea" to 120
)
println(prices) // {coffee=250, tea=120}
}
Це лише стартова точка. Далі ми обходитимемо такі мапи різними способами — залежно від завдання.
Нюанс: map[key] повертає V?
Коли ви читаєте значення за ключем, ви ніби запитуєте в мапи: «А чи є в тебе такий ключ?». Якщо ключа немає, мапа не може «вгадати» значення, тому повертається null. Саме тому вираз map[key] має тип V?, навіть якщо V сам по собі не nullable.
Наживо це виглядає так:
fun main() {
val ages = mapOf("Ann" to 20, "Bob" to 31)
val annAge = ages["Ann"]
val kateAge = ages["Kate"]
println(annAge) // 20
println(kateAge) // null
}
І ось тут багато новачків потрапляють у пастку: починають обходити ключі, а значення дістають через map[key], і раптом натрапляють на null. У цій лекції ми розберемо способи обходу, які роблять такий код простішим, безпечнішим і читабельнішим.
2. Представлення Map: keys, values, entries
Коли ви обходите Map, зазвичай вам потрібно одне з трьох: пройтися ключами, пройтися значеннями або пройтися парами повністю. Kotlin робить це явно: у мапи є keys, values і entries. Це не три різні мапи, а «вікна» (представлення) на одну й ту саму структуру — просто з різним поглядом на дані.
Зафіксуймо це в невеликій таблиці. Вона стане вам у пригоді щоразу, коли рука потягнеться «обходити Map як List».
| Що потрібно зробити | Що обходити | Що отримуємо в циклі |
|---|---|---|
| Потрібні лише ключі | |
|
| Потрібні лише значення | |
|
| Потрібні ключ і значення разом | |
|
| Потрібні ключ і значення разом (короткий синтаксис) | |
|
Обхід ключів: for (k in map.keys)
Іноді для логіки взагалі не важливі значення — головне знати набір ключів. Наприклад, ви хочете вивести список категорій у нашому навчальному консольному застосунку (умовному «трекері витрат») або принципово перевірити, чи є категорія "food".
fun main() {
val totalsByCategory = mapOf(
"food" to 1200,
"transport" to 450
)
for (category in totalsByCategory.keys) {
println(category)
}
// food
// transport
}
Плюс цього способу в тому, що він чесно повідомляє читачеві: «Я працюю з ключами». Мінус такий: якщо вам потім усе одно знадобляться значення, ви або зробите другий прохід, або почнете діставати map[category], і знову повернеться тема V?.
Обхід значень: for (v in map.values)
Якщо вас цікавлять лише значення, можна обходити values. Наприклад, ви хочете порахувати загальну суму за всіма категоріями (поки без просунутих операцій з колекціями — просто в циклі).
fun main() {
val totalsByCategory = mapOf(
"food" to 1200,
"transport" to 450
)
var sum = 0
for (value in totalsByCategory.values) {
sum += value
}
println("Усього = $sum") // Усього = 1650
}
Цей спосіб хороший, коли ключі справді не потрібні. І так, таке трапляється частіше, ніж здається: ключі нерідко потрібні «для красивого виводу», але не для обчислень.
Обхід пар: for (entry in map.entries)
Якщо потрібні і ключ, і значення, найпряміший шлях — обходити entries. Кожен елемент entries — це Map.Entry<K, V>, тобто об’єкт-пара з полями key і value.
fun main() {
val totalsByCategory = mapOf(
"food" to 1200,
"transport" to 450
)
for (entry in totalsByCategory.entries) {
println("${entry.key} -> ${entry.value}")
}
// food -> 1200
// transport -> 450
}
Цей варіант зазвичай виграє в «обході ключів + map[key]», бо ви не робите повторного пошуку за ключем і не змушуєте себе мати справу з V?.
Що таке Map.Entry<K, V> і чим він відрізняється від Pair
Map.Entry — це «пакування» однієї пари всередині Map. Важливо розуміти: Entry — не обов’язково Pair. Це окремий тип, який передає сенс саме «елемент мапи». Він дає доступ до entry.key і entry.value, а головне — дозволяє Kotlin і стандартній бібліотеці будувати навколо Map передбачувані API.
Щоб закріпити, ось невеличка схема «хто з кого складається»:
flowchart TD
M["Map⟨K, V⟩"] --> KEYS["keys: Set⟨K⟩"]
M --> VALUES["values: Collection⟨V⟩"]
M --> ENTRIES["entries: Set⟨Entry⟨K, V⟩⟩"]
ENTRIES --> E["Entry⟨K, V⟩"]
E --> K["key: K"]
E --> V["value: V"]
Практична думка: коли ви обходите entries, вам майже ніколи не потрібно знову лізти в мапу за ключем. Ви вже «стоїте» на парі, тримаєте її обома руками й можете працювати напряму.
3. Деконструкція та вибір стилю обходу
Деконструкція: for ((k, v) in map)
Іноді запис entry.key / entry.value здається трохи багатослівним. Kotlin дозволяє «розібрати» пару на дві змінні прямо в for. Це називається деконструкцією, і у випадку Map вона виглядає дуже природно: for ((k, v) in map).
Важливо: коли ви пишете for ((k, v) in map), ви обходите саме пари, просто в компактнішому синтаксисі.
fun main() {
val totalsByCategory = mapOf(
"food" to 1200,
"transport" to 450
)
for ((category, total) in totalsByCategory) {
println("$category: $total")
}
// food: 1200
// transport: 450
}
Цей варіант зазвичай найчитабельніший, якщо вам потрібні обидві частини пари й ви не робите всередині циклу нічого надто складного. До того ж він підштовхує давати змінним влучні імена: category, total, name, age.
Як обрати стиль обходу
Правило, яке економить багато нервів: якщо вам потрібна лише одна частина даних, обходьте лише її (keys або values). Якщо вам потрібно і те, і інше — обходьте пари (entries або деконструкція). Так код стає прямолінійнішим, а помилки — менш імовірними.
Порівняймо два підходи на одному прикладі: виведемо category -> total.
Підхід «за ключами, а значення дістану потім»:
fun main() {
val totalsByCategory = mapOf("food" to 1200, "transport" to 450)
for (category in totalsByCategory.keys) {
val total = totalsByCategory[category]
println("$category -> $total")
}
// food -> 1200
// transport -> 450
}
Він працює, але тут total має тип Int?, і це трохи дратує.
Підхід «за парами»:
fun main() {
val totalsByCategory = mapOf("food" to 1200, "transport" to 450)
for ((category, total) in totalsByCategory) {
println("$category -> $total")
}
// food -> 1200
// transport -> 450
}
Тут total — звичайний Int, і це рівно те, що ми хочемо у 99% випадків.
4. Практика: звіт за категоріями
Давайте додамо цю тему в той самий застосунок, який ми тягнемо через курс: консольний трекер витрат. Ми поки не використовуємо ООП, тому витрати можна зберігати простим Triple<String, String, Int>: (category, title, amount). Так, це неідеально, але зараз наше завдання — тренувати колекції та обхід, а не дизайн доменної моделі.
Нижче — невеликий шматок логіки: ми вже маємо список витрат і хочемо побудувати Map «категорія → сума», а потім гарно вивести звіт. Просунуті операції з колекціями (це буде пізніше) поки не використовуємо — зробимо все «чесним» циклом.
fun buildTotalsByCategory(expenses: List<Triple<String, String, Int>>): Map<String, Int> {
val totals = mutableMapOf<String, Int>()
for (e in expenses) {
val category = e.first
val amount = e.third
val oldTotal = totals[category] ?: 0
totals[category] = oldTotal + amount
}
return totals
}
Зверніть увагу на totals[category] ?: 0. Ми читаємо з Map, отримуємо Int? і, якщо там null (категорії ще немає), вважаємо стару суму рівною нулю. Це класичний патерн для Map. Він добре поєднується з тим фактом, що доступ за ключем повертає V?.
Тепер — функція друку звіту. І ось тут нам дуже знадобиться обхід Map через деконструкцію:
fun printTotalsReport(totalsByCategory: Map<String, Int>) {
println("Підсумки за категоріями:")
for ((category, total) in totalsByCategory) {
println("- $category: $total")
}
}
А тепер зберімо це в мікросценарій:
fun main() {
val expenses = mutableListOf(
Triple("food", "coffee", 250),
Triple("food", "sandwich", 350),
Triple("transport", "metro", 70)
)
val totals = buildTotalsByCategory(expenses)
printTotalsReport(totals)
// Підсумки за категоріями:
// - food: 600
// - transport: 70
}
Нюанс: порядок обходу Map краще не перетворювати на «частину логіки»
Коли ви виводите звіт, іноді хочеться, щоб категорії йшли «у тому порядку, в якому їх додавали». На практиці це може виглядати так, ніби порядок у Map є. Іноді він справді зберігатиметься, бо стандартна реалізація MutableMap у Kotlin — LinkedHashMap, яка під час ітерації зберігає порядок вставки. Але інша реалізація (HashMap) нічого не обіцяє щодо порядку.
Практичне правило: не робіть так, щоб правильність програми залежала від порядку обходу Map. Якщо порядок важливий як частина вимог, це окрема задача, і її треба розв’язувати явно, а не за принципом «ну наче ж друкується гарно».
7. Типові помилки під час обходу Map
Помилка №1: спроба обходити Map за індексами, «як список».
Іноді мозок за звичкою шукає map[0], map[1], map.indices. Але в мапи немає індексів: вона не про позиції, а про відповідності «ключ → значення». Якщо вам потрібен «номер рядка» під час виведення звіту, нумерацію можна зробити окремим лічильником. А сам обхід усе одно має йти через keys/values/entries або деконструкцію.
Помилка №2: обходять keys, а значення дістають через map[key] і дратуються через V?.
Це не «каприз Kotlin», а чесна модель: ключ може бути відсутнім. Якщо вам потрібні пари — обходьте пари. for ((k, v) in map) або for (entry in map.entries) зазвичай повністю знімають проблему, бо в циклі ви вже тримаєте і ключ, і значення — без повторного пошуку.
Помилка №3: пишуть for (k in map.keys) println(map[k]!!).
Це небезпечна звичка. Так, у конкретній ситуації «ключі ж із цієї ж мапи», тому map[k] справді не null. Але ви звикаєте розв’язувати питання null через !!, і потім ця звичка вистрілить в іншому місці — там, де ключі приходять ззовні (наприклад, із вводу користувача). Краще відразу обирати стиль обходу, який не змушує вас доводити очевидне компілятору.
Помилка №4: обходять values, а потім раптом потрібно «зрозуміти, до якого ключа належить значення».
Так буває, коли ви почали з «мені потрібні лише суми», а потім захотіли красиво вивести «категорія: сума». Якщо ключі справді знадобилися, значить, треба обходити пари. values корисні, але лише тоді, коли ви впевнені, що ключі взагалі не беруть участі в логіці.
Помилка №5: будують сенс програми на порядку обходу Map.
Сьогодні все друкується «як треба», завтра ви змінили реалізацію мапи або дані почали приходити в іншому порядку — і вивід «поїхав». Правильна програма не повинна залежати від випадкового порядку. Якщо порядок важливий, його потрібно проєктувати окремо, а не сподіватися на «ну в мене ж зараз працює».
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ