Коллекции: список, словарь, множество
Зачем это нужно
Класс из прошлой главы описывает одного участника. Но команда — это пятеро, таблица результатов — это тридцать строк, а список городов-участников — это сотня названий, среди которых не должно быть повторов.
Заводить под каждого отдельную переменную бессмысленно по той же причине, по которой мы отказались от name1, score1: их количество заранее неизвестно, и перебрать их циклом нельзя.
Для этого есть три вида хранилищ, и различаются они тем, как из них достают данные:
List— по порядковому номеру: первый, второй, третий. Порядок есть, повторы разрешены.Map— по ключу: «сколько очков у Алисы». Порядок неважен, ключи уникальны.Set— никак: из него нельзя достать «третий элемент», зато он сам следит, чтобы значения не повторялись.
В этой главе разберём все три, научимся обходить их циклом и преобразовывать цепочкой filter и map — той самой, которой в главе про функции мы отбирали значения.
Списки: List и MutableList
Теперь, когда есть тип для одного участника, соберём их в команду. List — упорядоченная коллекция элементов одного типа: у каждого элемента есть индекс (номер позиции), начиная с 0.
fun main() {
val names = listOf("Алиса", "Богдан", "Соня")
for (name in names) {
println(name)
}
println(names[1])
println(names.size)
}
Разбор по строкам
- Строка 2:
listOf("Алиса", "Богдан", "Соня")— функция-фабрика (какmapOf/setOfдальше в главе): создаётList<String>— список строк. Список, созданный черезlistOf, — read-only: в него нельзя ни добавить, ни убрать элемент (попробуем и увидим ошибку компиляции в разделе «Типичные ошибки»). - Строки 3–5:
for (name in names) { println(name) }— цикл проходит по всем трём элементам по очереди: сначалаname = "Алиса", затемname = "Богдан", затемname = "Соня", печатая каждый. - Строка 6:
names[1]— новый символ, квадратные скобки[ ]— оператор индексации (index operator): достаёт элемент списка по номеру позиции. Отсчёт всегда идёт с нуля:names[0]— «Алиса»,names[1]— «Богдан»,names[2]— «Соня».names[1]— это не то же самое, что «второй по счёту» в обычной речи, а «элемент под индексом 1». - Строка 7:
names.size— свойствоsize(без скобок — это не метод, а свойство, готовое значение) хранит количество элементов.
Что выведет:
Алиса
Богдан
Соня
Богдан
3
Первые три строки — цикл, четвёртая — names[1] («Богдан» под индексом 1), пятая — names.size (в списке три элемента).
Списки можно строить и из своих классов — например, из Competitor, а не только из строк и чисел. Тогда к каждому элементу применимы уже знакомые из главы 1 map/filter/sum.
fun main() {
val team = listOf(
Competitor("Алиса", 82),
Competitor("Богдан", 91),
Competitor("Соня", 77)
)
val totalScore = team.map { it.score }.sum()
println(totalScore)
}
data class Competitor(val name: String, var score: Int)
Разбор по строкам
- Строки 2–6:
val team = listOf(...)— список из трёх объектовCompetitor, каждый со своим именем и очками. Список можно писать в несколько строк — запятая после каждого элемента, кроме последнего, работает так же, как и в одну строку. - Строка 7:
team.map { it.score }.sum()— та же цепочкаmap/sumиз главы 1, только теперьit— не число, а целый объектCompetitor.it.scoreдостаёт из каждого объекта одно свойство,mapсобирает из этих свойств новый список чисел,sum()складывает их.
Что выведет: 250 — потому что 82 + 91 + 77 = 250.
Та же команда, но теперь цепочка filter/map из главы 1 — предскажи, какой список напечатается:
fun main() {
val team = listOf(
Competitor("Алиса", 82),
Competitor("Богдан", 91),
Competitor("Соня", 77)
)
val qualified = team.filter { it.score >= 80 }.map { it.name }
println(qualified)
}
data class Competitor(val name: String, var score: Int)Что напечатает этот код? Сначала ответь, потом проверяй.
Та же цепочка — теперь в замедленной съёмке: прогони каждый элемент через конвейер filter/map по шагам и посмотри, где элемент отбрасывается, а где превращается.
team.filter { it.score >= 80 }.map { it.name }filter { it.score >= 80 }map { it.name }Список из listOf — неизменяемый (в том смысле, что через сам этот List нельзя ни добавить, ни удалить элемент). Когда список должен расти и меняться, используют mutableListOf — тот же список, но с методами add/remove.
fun main() {
val roster = mutableListOf("Алиса", "Богдан")
roster.add("Соня")
roster.remove("Алиса")
println(roster)
}
Разбор по строкам
- Строка 2:
mutableListOf("Алиса", "Богдан")— создаётMutableList<String>: список с теми же методами, что у обычногоList(size,[индекс],map,filter...), плюс методы изменения. - Строка 3:
roster.add("Соня")— добавляет элемент в конец списка. - Строка 4:
roster.remove("Алиса")— убирает из списка первое вхождение значения"Алиса"(не по индексу, а по значению). - Строка 5:
println(roster)— списки печатаются в квадратных скобках через запятую — то же текстовое представление, что мы уже видели в главе 1 у результатаmap.
Что выведет: [Богдан, Соня] — «Соня» добавилась в конец, «Алиса» удалилась, порядок остальных элементов сохранился.
Map: пары ключ — значение
Map хранит не просто значения, а пары «ключ → значение» (key-value pair): вместо доступа по номеру позиции, как у List, доступ идёт по ключу — например, по имени участника.
fun main() {
val scores = mapOf("Алиса" to 82, "Богдан" to 91, "Соня" to 77)
println(scores["Богдан"])
for ((name, score) in scores) {
println("$name: $score")
}
}
Разбор по строкам
- Строка 2 — создание карты: три пары «имя → очки».
- Строка 3:
scores["Богдан"]— тот же оператор индексации[ ], что и у списков, только теперь в скобках не номер позиции, а ключ. Возвращает значение, связанное с этим ключом. - Строка 4:
for ((name, score) in scores)— перебор карты: на каждой итерацииforдаёт не один элемент, а пару, которую сразу деструктурируют (та же деструктуризация, что мы видели уdata class) наname(ключ) иscore(значение). - Строка 5:
println("$name: $score")— строковый шаблон из главы 1, теперь с двумя подстановками подряд.
Что выведет:
91
Алиса: 82
Богдан: 91
Соня: 77
Первая строка — прямой доступ по ключу "Богдан". Дальше — цикл: mapOf сохраняет порядок, в котором пары были перечислены при создании, поэтому вывод идёт именно в порядке Алиса → Богдан → Соня, а не в каком-то случайном.
Как и у List, есть изменяемая версия — MutableMap, с методами добавления и обновления записей.
fun main() {
val scores = mutableMapOf("Алиса" to 82, "Богдан" to 91)
scores["Соня"] = 77
scores["Алиса"] = 85
println(scores)
println(scores.getOrDefault("Дима", 0))
}
Разбор по строкам
- Строка 3:
scores["Соня"] = 77— новая роль тех же квадратных скобок: индексация слева от=не читает значение, а кладёт его в карту. Если ключа"Соня"ещё не было — добавляет новую пару; если был бы — заменил бы значение. - Строка 4:
scores["Алиса"] = 85— ключ"Алиса"уже существовал (со значением 82), поэтому это не добавление, а замена: новое значение 85 затирает старое 82. - Строка 6:
scores.getOrDefault("Дима", 0)— безопасный способ спросить значение по ключу, которого может не быть. Если бы мы написали простоscores["Дима"], то получили быnull(«ничего») — проnullв этом курсе будет отдельная глава, а покаgetOrDefaultобходит эту проблему: если ключа нет, возвращает второй аргумент как запасное значение.
Что выведет:
{Алиса=85, Богдан=91, Соня=77}
0
Карта печатается в фигурных скобках, пары через запятую в формате ключ=значение. "Алиса" теперь 85, а не 82 — значение заменилось. Для несуществующего ключа "Дима" вернулся запасной 0.
Set: только уникальные значения
Set — коллекция без повторов: если добавить значение, которое уже есть, ничего не произойдёт — набор просто не станет длиннее.
fun main() {
val uniqueScores = setOf(82, 91, 82, 77, 91)
println(uniqueScores)
println(uniqueScores.size)
}
Разбор по строкам
- Строка 2:
setOf(82, 91, 82, 77, 91)— передали пять чисел, но82и91встречаются дважды.Setхранит только уникальные значения, поэтому повторы схлопываются в один экземпляр каждое. - Строка 3–4 — печать:
Set, как иList, выводится в квадратных скобках, порядок — тот, в котором значения встретились впервые.
Что выведет:
[82, 91, 77]
3
Из пяти переданных чисел осталось три уникальных: 82 и 91 каждое учтено один раз, несмотря на то что были указаны дважды. Как и у списков и карт, есть mutableSetOf с методами add/remove — работает по тем же правилам, что и mutableListOf.
Именно здесь особенно важна связка equals()/hashCode() из раздела про data class: Set и ключи Map определяют уникальность через них. У data class она согласована автоматически — два структурно одинаковых объекта Competitor в Set схлопнутся в один, как схлопнулись одинаковые числа выше. У обычного класса это не сработает: два объекта с одинаковыми полями будут считаться разными, потому что сравниваются по ссылке.
Типичные ошибки
Сообщения ниже сняты вживую с той же песочницы Kotlin Playground, что работает в кнопках Run этой главы.
3. add() у List, а не у MutableList
fun main() {
val names: List<String> = listOf("Алиса", "Богдан")
names.add("Соня")
}
Unresolved reference 'add'.
Перевод: «Неразрешённая ссылка add». List — интерфейс только для чтения: у него нет метода add, он просто не существует у этого типа, поэтому компилятор его не находит — так же, как не нашёл бы опечатку в имени переменной.
Как починить: объявить список как изменяемый — val names: MutableList<String> = mutableListOf("Алиса", "Богдан"), — и тогда add появится.
5. Map возвращает значение, которого может не быть
fun main() {
val scores = mapOf("Алиса" to 82, "Богдан" to 91)
val aliceScore: Int = scores["Алиса"]
println(aliceScore)
}
Initializer type mismatch: expected 'Int', actual 'Int?'.
Перевод: «Несовпадение типа начального значения: ожидался Int, а по факту Int?». Обращение к карте по ключу через [ ] всегда возвращает «может быть Int, а может быть ничего» (Int? — nullable-тип, потому что ключа могло и не быть) — даже если ключ в этот раз точно есть. Компилятор судит по типу, а не по конкретным данным, а тип scores["Алиса"] — всегда Int?, не Int.
Как починить: либо изменить тип переменной на Int? — val aliceScore: Int? = scores["Алиса"], — либо использовать getOrDefault("Алиса", 0) или оператор !! (разберём в будущей главе про null-безопасность), если ты точно уверен, что ключ есть.
Символы главы
| Символ / конструкция | Как называется | Что делает | Пример |
|---|---|---|---|
listOf / mutableListOf | создание списка | первый — только для чтения, второй — с методами add и remove | listOf("Алиса", "Богдан") |
[индекс] | оператор индексации | достаёт элемент списка по позиции, начиная с нуля | names[1] |
for (x in coll) { } | цикл по коллекции | присваивает x каждый элемент коллекции по очереди | for (name in names) |
.filter { } / .map { } | преобразования | отбирает элементы по условию и превращает каждый в другое значение | team.filter { it.score >= 75 } |
mapOf / mutableMapOf | создание карты | хранилище пар ключ — значение | mapOf("Алиса" to 82) |
to | инфиксная функция | создаёт пару ключ-значение (Pair) для mapOf | "Алиса" to 82 |
[ключ] | чтение из карты | возвращает значение или null, если ключа нет — поэтому тип с вопросом | scores["Алиса"] |
[ключ] = значение | индексация с присваиванием | добавляет или заменяет значение в MutableMap по ключу | scores["Соня"] = 77 |
for ((k, v) in map) | обход карты с деструктуризацией | раскладывает каждую пару на ключ и значение | for ((name, score) in scores) |
setOf / mutableSetOf | создание множества | хранит только уникальные значения, порядок не гарантирован | setOf("Самара", "Самара") |
Углубиться
List — это интерфейс, а не гарантия неизменности. listOf(...) возвращает объект, у типа List которого просто нет методов изменения — но это ограничение на уровне доступа через этот тип, а не свойство самих данных в памяти. Если тот же набор элементов виден откуда-то ещё и через MutableList (например, mutableListOf присвоили переменной типа List), изменить его всё ещё можно — просто не через ссылку List. Для настоящей неизменности данных в памяти в Kotlin есть отдельные структуры за пределами базового курса.
Почему Set не хранит порядок. Внутри он устроен на хеш-таблице: значение попадает в «корзину» по своему hashCode(), и порядок обхода определяется расположением корзин, а не порядком добавления. Если порядок нужен — есть отдельный вид множества, который его запоминает.
Дальше по теме:
- Collections overview — официальная документация: List, Set, Map и их изменяемые версии.
- Collection operations — полный список преобразований, включая filter, map, groupBy и associate.
- Map-specific operations — чтение с значением по умолчанию, обход, слияние карт.
Слова главы
Потренируйся печатать
Цель: скорость ≥ 120 зн/мин, точность ≥ 90%
val scores = mapOf("Алиса" to 82, "Богдан" to 91)
Челлендж ⭐
Команда Competitor уже знакома: Алиса — 82 очка, Богдан — 91, Соня — 66.
Ступень 1. Организаторам нужен список имён тех, кто прошёл отбор — набрал score >= 75. Напиши одно выражение на цепочке .filter { }.map { } (по образцу из этой главы и главы 1), которое из списка team отбирает нужных участников и оставляет только их имена.
Ступень 2 ⭐. Напиши функцию medalFor(score: Int): String, использующую when с диапазонами (>= 90 — "Золото", >= 75 — "Серебро", >= 50 — "Бронза", иначе — "Участие"), и выведи для каждого участника команды строку вида Богдан: Золото.
fun main() {
val team = listOf(
Competitor("Алиса", 82),
Competitor("Богдан", 91),
Competitor("Соня", 66)
)
// ступень 1: замени emptyList() на список имён с score >= 75
val qualified = emptyList<String>()
println("Прошли отбор: $qualified")
// ступень 2 (со звёздочкой): допиши medalFor и выведи медаль каждого участника
}
data class Competitor(val name: String, var score: Int)
// ступень 2 (со звёздочкой): допиши функцию здесь
// fun medalFor(score: Int): String = when { ... }
Решение (сначала попробуй сам)
Ступень 1:
val qualified = team.filter { it.score >= 75 }.map { it.name }
Ступень 2 ⭐:
fun medalFor(score: Int): String = when {
score >= 90 -> "Золото"
score >= 75 -> "Серебро"
score >= 50 -> "Бронза"
else -> "Участие"
}
fun main() {
val team = listOf(
Competitor("Алиса", 82),
Competitor("Богдан", 91),
Competitor("Соня", 66)
)
val qualified = team.filter { it.score >= 75 }.map { it.name }
println("Прошли отбор: $qualified")
for (competitor in team) {
println("\${competitor.name}: \${medalFor(competitor.score)}")
}
}
data class Competitor(val name: String, var score: Int)
Полный вывод:
Прошли отбор: [Алиса, Богдан]
Алиса: Серебро
Богдан: Золото
Соня: Бронза
Алиса (82) и Богдан (91) прошли отбор — оба набрали 75 и больше; Соня с 66 — нет, поэтому в qualified её не будет, но when всё равно посчитает ей медаль «Бронза» (66 ≥ 50) при выводе полного списка.
Куда дальше: проверенные ресурсы
Русские материалы с нуля
- kotlinlang.ru — Классы — официальная документация в переводе: конструкторы, свойства, наследование — читай раздел про основной конструктор и свойства.
- kotlinlang.ru — Классы данных — тот же перевод про data class: что именно генерируется и какие есть ограничения на параметры конструктора.
- kotlinlang.ru — Коллекции. Общий обзор — иерархия типов коллекций Kotlin: чем List/Set/Map отличаются от их изменяемых версий на уровне интерфейсов.
- kotlinlang.ru — Условия и циклы — в этой же главе официального перевода, ниже if, разобран when с примерами диапазонов и множественных веток.
- Урок 11: ООП. Классы и объекты в Kotlin — продолжение курса с уроков 1 и 10: классы объясняются через бытовые аналогии, с видео.
- Урок 9: Списки в Kotlin, функции для работы с коллекциями — тот же курс: List и операции над коллекциями отдельным уроком, с видео.
Официальный тур Kotlin (на английском)
- Kotlin tour: Classes — официальный тур: конструкторы, свойства и методы со встроенными упражнениями.
- Kotlin tour: Collections — официальный тур: List, Set, Map и их изменяемые версии на практических примерах.
Потренироваться
-
Kotlin Koans: Classes / Collections — в общем списке разделов Koans есть отдельные категории Classes и Collections — задачи с автопроверкой именно на темы этой главы.
-
Видео и материалы сообщества — ниже — ролики по теме и ссылки от студентов и преподавателей смотри в самом низу страницы.
Сводная проверка по всей главе: 6 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.
Что должен уметь
- Отличать
ListотMutableListиMapотMutableMapпо набору доступных методов, объясняя, почему.add()не работает наList. - Создавать и обходить
List,MapиSet, используя[индекс]/[ключ],for (x in coll)и деструктуризациюfor ((k, v) in map). - Строить пары ключ-значение через инфиксную функцию
toи безопасно читать значение из карты черезgetOrDefaultили?:. - Объяснять, почему обращение к
Mapпо ключу даёт тип с вопросом, и чинить ошибку компиляции, которая из этого следует. - Объяснять, почему
Setотбрасывает повторы и не даёт доступа по номеру. - Собирать цепочку
.filter { }.map { }и говорить, что делает каждое звено.
Комментарии
Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.