1. Навіщо потрібен Map і як читати Map<K, V>
Коли ви лише звикаєте до колекцій, дуже легко мислити так: «Мені потрібен список, отже — List». І це правда… доки ви не стикаєтеся із завданням, де ключ до даних — це не позиція (0, 1, 2…), а якась осмислена мітка: назва товару, команда, логін, категорія, ID.
Map<K, V> (мапа, словник) зберігає пари «ключ → значення». Ключі унікальні: два однакові ключі в одній мапі існувати не можуть. Натомість значення можуть повторюватися скільки завгодно: наприклад, товари можуть мати однакову ціну, але ключі (назви товарів) усе одно різні.
Ключовий момент: Map у Kotlin — це окремий «світ». Він не наслідується від Collection, хоча й належить до колекцій як тип даних стандартної бібліотеки.
Як читати тип Map<K, V> і не лякатися кутових дужок
Коли ви бачите Map<String, Int>, мозок спочатку вдає, що зайнятий: «давай потім, я не готовий». Це нормально. Тут важливо навчитися читати тип уголос — як речення. Map<K, V> читається як «мапа від ключів типу K до значень типу V». Тобто Map<String, Int> — це «ключі — рядки, значення — цілі числа».
У реальному житті це може бути, наприклад, «назва товару → ціна», «категорія → сума витрат», «слово → кількість разів, коли воно трапилося». Жодної магії: кутові дужки просто фіксують, що саме ми кладемо й дістаємо.
Подивімося на «кишеньковий прайс-лист»:
fun main() {
val prices: Map<String, Int> = mapOf(
"tea" to 90,
"coffee" to 150
)
println(prices) // {tea=90, coffee=150}
}
Зверніть увагу на to. Це зручний спосіб створити пару «ключ → значення», щоб mapOf(...) читався як «перелік відповідностей».
2. Читання з Map: чому отримуємо V? і як із цим жити
Якщо ви звикли до List, то очікуєте логіку «дай елемент за індексом — і отримаєш елемент». Але в Map ключ може бути відсутнім. І Kotlin робить дуже правильну річ: замість того щоб «вибухнути» помилкою, читання за ключем повертає null, якщо ключ не знайдено.
Саме тому вираз map[key] має тип V?, навіть якщо V сам по собі не допускає null. Це одна з найважливіших деталей, і вона постійно траплятиметься вам, доки ви пишете будь-які програми з Map.
fun main() {
val prices = mapOf("tea" to 90, "coffee" to 150)
val a: Int? = prices["tea"]
val b: Int? = prices["milk"]
println(a) // 90
println(b) // null
}
Тут немає «помилки». milk просто відсутній — і це нормальна ситуація, а не катастрофа.
Elvis ?: під час читання: «якщо ключа немає — беріть значення за замовчуванням»
Після того як ви прийняли реальність V?, виникає наступне питання: «Гаразд, а що робити, якщо null мені не підходить?» У простих сценаріях найкраще працює оператор Елвіса ?:: «якщо зліва null, то візьміть справа».
Це особливо зручно для чисел, коли у вас є «природний нуль». Наприклад, якщо немає ціни — вважати ціною 0 (або «невідомо», але це вже інше рішення).
fun main() {
val prices = mapOf("tea" to 90, "coffee" to 150)
val milkPrice = prices["milk"] ?: 0
val teaPrice = prices["tea"] ?: 0
println(milkPrice) // 0
println(teaPrice) // 90
}
Тут ми явно обираємо політику: «немає ключа → 0». Важливо, що Kotlin змушує вас ухвалити рішення, а не вдавати, ніби ключ «точно є».
Перевірка наявності ключа: key in map як читабельний стиль
Іноді значення за замовчуванням не підходить. Наприклад, якщо команда не існує, ви хочете вивести повідомлення про помилку, а не «тихо» продовжити роботу. Тоді спочатку перевіряємо, чи є ключ у мапі.
Kotlin дозволяє писати це дуже читабельно: if (key in map). За змістом це означає: «якщо ключ входить до набору ключів мапи».
fun main() {
val prices = mapOf("tea" to 90, "coffee" to 150)
val item = "tea"
if (item in prices) {
println("Ціна: ${prices[item]}") // Ціна: 90
} else {
println("Немає такого товару") // (не виконається)
}
}
Тут prices[item] усе ще повертає Int?, але в такому коді ви зазвичай уже впевнені, що ключ є. Трохи пізніше в курсі ви навчитеся робити це ще акуратніше, але на поточному етапі цього достатньо: перевірка + зрозуміла гілка поведінки.
3. MutableMap: запис, оновлення та видалення
До цього ми використовували mapOf(...), тобто Map лише для читання. Її завдання — «зберігати й давати читати». Але якщо вам потрібно змінювати дані під час роботи застосунку, беріть MutableMap.
У Kotlin цей поділ такий самий, як у List: тип лише для читання (Map) і змінюваний тип (MutableMap). Водночас MutableMap підтримує операції запису: можна додавати нові пари й оновлювати наявні значення.
Одна форма для «додати» й «оновити»
Найприємніше — запис виглядає однаково і для «додати», і для «оновити»:
fun main() {
val stock = mutableMapOf<String, Int>()
stock["apple"] = 3
stock["apple"] = 4
stock["banana"] = 1
println(stock) // {apple=4, banana=1}
}
Тобто оператор [] для MutableMap працює в обидва боки: читання map[key] і запис map[key] = value. Це одна з причин, чому мапи в Kotlin відчуваються як «чесний словник», а не як щось громіздке.
Невелика практична деталь: стандартна реалізація MutableMap за замовчуванням — LinkedHashMap, і під час обходу вона зберігає порядок додавання. Сьогодні ми не будемо обходити мапу (це тема наступного дня), але корисно розуміти, чому під час println(map) ви часто бачите «пари в очікуваному порядку».
Видалення за ключем: remove(key) і «що саме повернулося»
Видалення в MutableMap зазвичай робиться так: remove(key). І тут Kotlin знову поводиться «по-дорослому»: метод повертає значення, яке було видалено, або null, якщо ключа не було.
Це зручно, бо одним виразом ви можете дізнатися, чи було що видаляти.
fun main() {
val stock = mutableMapOf("apple" to 4, "banana" to 1)
val removed: Int? = stock.remove("banana")
val removedAgain: Int? = stock.remove("banana")
println(removed) // 1
println(removedAgain) // null
println(stock) // {apple=4}
}
Знову та сама філософія: «відсутність ключа» — нормальний стан світу, а не виняток.
Міні-таблиця: що повертають базові операції Map
Іноді легше один раз побачити «контракт» операцій, ніж десять разів читати пояснення. У цьому розділі ми акуратно зафіксуємо, які типи повертаються, щоб ви перестали дивуватися ? у несподіваних місцях (це здивування — одне з найчастіших джерел роздратування в новачків).
| Операція | Приклад | Що повертається | Чому |
|---|---|---|---|
| Читання за ключем | |
|
ключ може бути відсутнім |
| Перевірка ключа | |
|
лише факт наявності |
| Запис/оновлення | |
|
це дія, а не «значення» |
| Видалення | |
|
можна дізнатися, що видалили |
Цю таблицю не «треба вчити напамʼять», а «треба звикнути». Через пару вправ мозок почне автоматично очікувати nullable під час читання з мапи.
4. Патерн «лічильник»: збільшуємо значення за ключем
Зараз буде один із найпрактичніших прийомів. Він трапляється всюди: статистика, підрахунки, частоти, «скільки разів користувач зробив X», «яка сума витрат у категорії Y». І робиться це буквально в один рядок.
Проблема звучить так: «Я хочу збільшити лічильник за ключем. Але якщо ключа немає, лічильник має вважатися рівним 0».
Рішення: (map[key] ?: 0) + 1, після чого записуємо результат назад.
fun main() {
val counts = mutableMapOf<String, Int>()
val word = "kotlin"
counts[word] = (counts[word] ?: 0) + 1
counts[word] = (counts[word] ?: 0) + 1
println(counts) // {kotlin=2}
}
Виглядає майже надто просто — але це саме та простота, заради якої люди люблять Kotlin: усе чесно, і null оброблено явно.
Приклад «лічильник слів» без функціональних ланцюжків
Дуже хочеться вже писати красиво через map/filter/groupBy, але ми домовилися: сьогодні без «операцій над колекціями», лише базові інструменти. Тому зробимо максимально зрозумілий варіант: читаємо рядок, розбиваємо на слова, нормалізуємо регістр і рахуємо частоти.
Ми рахуватимемо слова, розділені пробілами. Це не ідеальний парсер тексту (розділові знаки залишаться в словах), але для відпрацювання Map цього цілком достатньо.
fun main() {
val line = readln().trim().lowercase()
val parts = line.split(" ")
val counts = mutableMapOf<String, Int>()
for (p in parts) {
if (p != "") counts[p] = (counts[p] ?: 0) + 1
}
println(counts)
}
Якщо ввести:
Kotlin kotlin is fun
то виведення буде приблизно таким (порядок може збігатися з порядком додавання):
// {kotlin=2, is=1, fun=1}
Тут ми одночасно побачили три речі: нормалізацію рядка (trim().lowercase()), базовий цикл for і той самий патерн лічильника.
5. Приклад: «категорія → сума» і нормалізація ключів
Тепер акуратно привʼяжемо Map до нашого практичного контексту: ми поступово рухаємося до застосунку, який зберігає записи (наприклад, витрати) у памʼяті. Поки без класів, тому запис подамо як Triple(category, title, amount). Сьогодні наша мета — не «ідеальна архітектура», а практика мапи: побудуємо звіт «за категоріями».
Зробимо функцію, яка за списком записів будує MutableMap<String, Int>, де ключ — категорія, а значення — сума витрат за категорією.
fun buildTotalsByCategory(
records: List<Triple<String, String, Int>>
): Map<String, Int> {
val totals = mutableMapOf<String, Int>()
for (r in records) {
val category = r.first
val amount = r.third
totals[category] = (totals[category] ?: 0) + amount
}
return totals
}
Зверніть увагу на хороший стиль для новачків: усередині ми використовуємо MutableMap, бо «будуємо» результат, а назовні повертаємо Map, бо «віддаємо читати». Це проста дисципліна: змінюємо там, де створюємо, і не обіцяємо змінюваності там, де вона не потрібна.
Перевірмо на маленькому прикладі:
fun main() {
val records = mutableListOf<Triple<String, String, Int>>()
records.add(Triple("food", "coffee", 300))
records.add(Triple("transport", "taxi", 1200))
records.add(Triple("food", "sandwich", 250))
val totals = buildTotalsByCategory(records)
println(totals) // {food=550, transport=1200}
}
Такий звіт — базова причина, чому Map зʼявляється в застосунках частіше, ніж «чисті множини», а інколи навіть частіше, ніж списки: дуже багато завдань зводяться до того, щоб «за ключем зберігати агреговане значення».
Нюанс про ключі: нормалізація рядка як частина контракту
Мапи чесно порівнюють ключі за точною рівністю. Для рядка це означає, що "Food", "food" і " food " — три різні ключі. І це не «проблема Kotlin», а ваша відповідальність як автора застосунку.
Тому, якщо ключ приходить від користувача (наприклад, він вводить категорію), хороше правило — нормалізувати його: trim() і часто lowercase(). Тоді ваша Map відображатиме зміст даних, а не випадкові пробіли й настрій Caps Lock.
Міні-приклад:
fun main() {
val totals = mutableMapOf<String, Int>()
val raw = " FOOD "
val category = raw.trim().lowercase()
totals[category] = (totals[category] ?: 0) + 100
println(totals) // {food=100}
}
Якщо ви не зробите таку нормалізацію, то в звіті раптом зʼявляться категорії Food, FOOD, food — і вам здаватиметься, що застосунок «зламався», хоча він просто надто буквально сприймає введення.
6. Типові помилки під час роботи з Map/MutableMap
Помилка №1: очікувати, що map[key] поверне V, а не V?.
Новачки часто пишуть val x: Int = prices["tea"] і дивуються помилці компіляції. Компілятор тут ваш союзник: він попереджає, що ключ може бути відсутнім. Виправлення зазвичай просте: або Elvis ?:, або перевірка if (key in map), або інший контракт функції (наприклад, повернути Int? назовні).
Помилка №2: «лікувати» V? через !!, бо «ну я ж знаю, що ключ є».
!! інколи здається коротким шляхом, але це як «я точно не впаду» перед катанням на самокаті без рук. Сьогодні ви впевнені, завтра вхідні дані змінилися — і застосунок падає. Набагато спокійніше або перевіряти ключ, або використовувати ?: зі зрозумілою політикою за замовчуванням.
Помилка №3: намагатися оновлювати Map, створену через mapOf.
mapOf(...) повертає Map лише для читання. Вона для читання, а не для змін. Якщо потрібно додавати, видаляти або оновлювати, використовуйте mutableMapOf(...). Це не прискіпування: поділ на read-only і mutable — важлива частина дизайну колекцій у Kotlin.
Помилка №4: забути про нормалізацію ключа й отримати «розмноження сутностей».
Коли ключ — рядок від користувача, пробіли й регістр перетворюються на різні ключі. У звіті зʼявляються дублікати «за змістом», і ви починаєте підозрювати щось дивне. Зазвичай достатньо домовитися: ключі зберігаємо в trim().lowercase(), а «гарне відображення» зробимо пізніше.
Помилка №5: зберігати в Map те, що за змістом має бути List.
Іноді хочеться все перетворити на мапу, бо «там зручно за ключем». Але якщо у вас дані природно впорядковані, якщо важливі повтори або порядок, якщо вам потрібен доступ за позицією — це завдання для List. Map добра тоді, коли ключ справді унікальний і за ним логічно звертатися як до «адреси».
Помилка №6: плутати «ключ» та «індекс» і намагатися мислити мапою як списком.
У Map немає «нульового елемента» і «останнього індексу» в тому сенсі, як у списку. Є лише ключі. Якщо ви ловите себе на бажанні «пробігтися по мапі за індексами», значить ви або розвʼязуєте завдання обходу (ми це розберемо пізніше), або вам насправді потрібен List.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ