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("Total = $sum") // Total = 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("Totals by category:")
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)
// Totals by category:
// - 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.
Сегодня всё печатается «как надо», завтра вы поменяли реализацию карты или данные стали приходить другим порядком — и вывод «поехал». Правильная программа не должна зависеть от случайного порядка. Если порядок важен, его нужно проектировать отдельно, а не надеяться на «ну у меня же сейчас работает».
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ