JavaRush /Курсы /Kotlin SELF /Обход Map: keys, values, entries, Map.Entry и деконструкц...

Обход Map: keys, values, entries, Map.Entry и деконструкция (k, v)

Kotlin SELF
20 уровень , 2 лекция
Открыта

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».

Что нужно сделать Что обходить Что получаем в цикле
Нужны только ключи
map.keys
K
Нужны только значения
map.values
V
Нужны ключ и значение вместе
map.entries
Map.Entry<K, V>
Нужны ключ и значение вместе (короткий синтаксис)
for ((k, v) in map)
(K, V)

Обход ключей: 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.
Сегодня всё печатается «как надо», завтра вы поменяли реализацию карты или данные стали приходить другим порядком — и вывод «поехал». Правильная программа не должна зависеть от случайного порядка. Если порядок важен, его нужно проектировать отдельно, а не надеяться на «ну у меня же сейчас работает».

1
Задача
Kotlin SELF, 20 уровень, 2 лекция
Недоступна
Меню консоли
Меню консоли
1
Задача
Kotlin SELF, 20 уровень, 2 лекция
Недоступна
Итог обучения
Итог обучения
1
Задача
Kotlin SELF, 20 уровень, 2 лекция
Недоступна
Отчёт по городам
Отчёт по городам
1
Задача
Kotlin SELF, 20 уровень, 2 лекция
Недоступна
Контакты друзей
Контакты друзей
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ