Коллекции и Sequence в Kotlin
Дальше: Типы данных и переменные · Справочник Kotlin
Структуры данных — о разделе → реализация и псевдокод операций → Коллекции в контексте ООП.
Ниже — List, Set, Map в Kotlin.
список.add(элемент)
значение := список[индекс]
карта[ключ] = значение
значение := карта[ключ]
множество.add(x) // Set — без дубликатов
Разбор:
список.add(элемент)показывает базовую операцию добавления в конец последовательной коллекции.список[индекс]иллюстрирует доступ по позиции, который характерен для List/Array.карта[ключ] = значениедемонстрирует запись в словарь по ключу.карта[ключ]читает связанное значение за счёт ассоциативного доступа.множество.add(x)подчёркивает ключевое свойство Set: хранение только уникальных элементов.
Play ITЗагрузка интерактивного демо…
Коллекции и Sequence
В программе редко хватает одной переменной — нужны списки заказов, словари "id → пользователь", множества уникальных тегов. В Kotlin для этого есть готовые типы в стандартной библиотеке.
Эта статья объясняет:
- чем отличаются
List,Set,Map; - зачем неизменяемые и изменяемые версии;
- когда подключать ленивый Sequence;
- как не словить типичные ошибки при итерации.
Справочник функций: встроенные функции. Асинхронные потоки во времени — отдельно Flow.
Словарь терминов
| Термин | Простыми словами |
|---|---|
| Коллекция | Структура из многих элементов (список, множество, карта). |
| List | Упорядоченный список; допускает повторы; доступ по индексу 0, 1, 2… |
| Set | Множество уникальных элементов без порядка (или с порядком в LinkedHashSet). |
| Map | Пары "ключ → значение": как телефонная книга по имени. |
Иммутабельный (listOf) | После создания "как будто нельзя менять" — изменения дают новую коллекцию. |
Мутабельный (mutableListOf) | Можно add, remove на месте. |
| Iterable | То, по чему можно пройти for (x in coll). |
| Sequence | Ленивая цепочка: элементы обрабатываются по одному, без лишних промежуточных списков. |
| Терминальный оператор | Завершает цепочку: toList(), count(), first() — после него Sequence "отработал". |
Сводка операций по типам
MutableList:
| Действие | Метод |
|---|---|
| Добавить в конец | add(value) |
| Вставить по индексу | add(index, value) |
| Прочитать | get(index), [index] |
| Заменить | set(index, value), [index] = value |
| Удалить по индексу | removeAt(index) |
MutableMap:
| Действие | Метод |
|---|---|
| Добавить или заменить | put(key, value), [key] = value |
| Прочитать | get(key), [key] |
| Удалить | remove(key) |
MutableSet:
| Действие | Метод |
|---|---|
| Добавить | add(value) |
| Удалить | remove(value) |
| Проверить наличие | contains(value) |
ArrayDeque (очередь/стек):
| Режим | Операции |
|---|---|
| Queue (FIFO) | addLast(x), removeFirst(), firstOrNull() |
| Stack (LIFO) | addLast(x) (push), removeLast() (pop), lastOrNull() (peek) |
| Deque | addFirst/addLast, removeFirst/removeLast |
Неизменяемые listOf / mapOf / setOf поддерживают чтение; "изменение" — новая коллекция (+, buildList, копия в mutable*).
Когда какой тип выбрать
| Задача | Тип |
|---|---|
| Список задач по порядку | List |
| Уникальные теги, посещённые id | Set |
| Поиск по ключу (id пользователя) | Map |
| Большой файл, обработка по строкам | Sequence |
| Обновления из сети/БД во времени | Flow |
Создание коллекций
val readOnly = listOf("a", "b", "c")
val mutable = mutableListOf(1, 2, 3)
val scores = mapOf("Ann" to 90, "Bob" to 85)
val unique = setOf(1, 2, 2, 3) // фактически {1, 2, 3}
Разбор:
- Вход: фрагмент создаёт четыре базовых типа коллекций Kotlin.
- Ключевые API:
listOf,mutableListOf,mapOf,setOfформируют read-only и mutable структуры. - Поток выполнения: коллекции инициализируются значениями при создании;
setOfсразу удаляет дубли. - Результат: получаем готовые контейнеры для разных задач — порядок, ключи, уникальность.
- Типичная ошибка: попытка вызвать
addнаlistOf(...), где интерфейс только для чтения.
| Функция | Можно менять на месте? |
|---|---|
listOf, mapOf, setOf | Нет (интерфейс read-only) |
mutableListOf, mutableMapOf | Да |
Неизменяемость и копии
val base = listOf(1, 2, 3)
val extended = base + 4 // новый List, base не меняется
val mutable = base.toMutableList()
mutable.add(4) // меняется только mutable
Разбор:
base + 4не изменяет исходный список, а создаёт новую коллекциюextended.toMutableList()копирует данные в изменяемую структуру для in-place операций.mutable.add(4)меняет только копию, поэтомуbaseостаётся прежним.- пример показывает безопасную модель "immutable по умолчанию + mutable по необходимости".
Оператор + для коллекций создаёт новый список. На тысячах элементов в цикле это дорого — тогда один раз buildList { }:
val ids = buildList {
addAll(source)
add(newId)
}
Разбор:
buildList { ... }создаёт список через внутренний mutable builder и возвращает read-only результат.addAll(source)добавляет исходные элементы одним вызовом.add(newId)дописывает новый элемент в конец в рамках той же сборки.- подход снижает число промежуточных аллокаций по сравнению с многократным
+в цикле.
Цепочки filter и map на List
data class User(val name: String, val isActive: Boolean)
val users = listOf(
User("Ann", true),
User("Bob", false),
User("Cal", true)
)
val activeNames = users
.filter { it.isActive }
.map { it.name }
Разбор:
data class User(...)даёт готовыеequals/hashCode/toStringдля удобной работы с коллекциями.filter { it.isActive }оставляет только активных пользователей.map { it.name }преобразует списокUserв список имён.activeNamesформируется как новая коллекция, оригинальныйusersне меняется.
Разбор выражения filter { it.isActive }:
filter— оставить элементы, для которых лямбда вернулаtrue.it— текущий элемент (User); имя можно заменить:filter { user -> user.isActive }.- Результат — новый список; исходный
usersне меняется.
У List каждый шаг (filter, map, sorted) строит новую коллекцию — это eager (жадное) выполнение.
Sequence — ленивая цепочка
val result = (1..1_000_000)
.asSequence()
.filter { it % 2 == 0 }
.map { it * it }
.take(10)
.toList()
Разбор:
(1..1_000_000)создаёт диапазон чисел.asSequence()переводит обработку в lazy-режим без мгновенного создания промежуточных списков.filterиmapприменяются к элементам по мере запроса downstream.take(10)ограничивает результат первыми десятью подходящими значениями.toList()терминально материализует итог в обычный список.
Пока не вызван toList(), count(), first() и т.п., элементы проходят цепочку по одному: для миллиона чисел не создаётся миллион промежуточных списков — берутся первые 10 подходящих чётных квадратов.
Чтение большого лога:
import java.io.File
fun topErrors(path: String, limit: Int): List<String> =
File(path).useLines { lines ->
lines.asSequence()
.filter { "ERROR" in it }
.take(limit)
.toList()
}
Разбор:
File(path).useLines { ... }открывает файл и гарантирует закрытие ресурса после блока.lines.asSequence()обрабатывает строки лениво, не загружая весь файл в память.filter { "ERROR" in it }оставляет строки, содержащие маркер ошибки.take(limit)ограничивает объём результата заданным числом строк.toList()возвращает материализованный результат для дальнейшего использования вызывающим кодом.
useLines закрывает файл после блока — sequence не должен "жить" дольше открытого потока.
Eager и Lazy — наглядно
// Eager: три полных прохода, три промежуточных списка в памяти
val names = users
.filter { it.isActive }
.map { it.name }
.sorted()
// Lazy: один проход при toList()
val namesLazy = users.asSequence()
.filter { it.isActive }
.map { it.name }
.sorted()
.toList()
Разбор:
- первая цепочка (
names) выполняет каждую стадию сразу и создаёт промежуточные списки. - вторая цепочка (
namesLazy) откладывает вычисления доtoList(). sorted()в обоих случаях сортирует данные, но lazy-вариант эффективнее для длинных пайплайнов.- пример помогает выбирать между простотой eager-обработки и экономией ресурсов в lazy-режиме.
List + filter/map | Sequence | |
|---|---|---|
| Когда считается | На каждом шаге сразу | При терминальном операторе |
| Память | Промежуточные списки | Один элемент за раз |
| Когда выбирать | До ~сотен–тысяч элементов, короткая цепочка | Длинная цепочка, большой файл, генератор |
Группировка и Map из списка
Код ITЗагрузка примера кода…
Разбор:
groupBy { it.region }формируетMap<String, List<Order>>с группами по региону.groupingBy { ... }.fold(0) { ... }сразу агрегирует сумму по каждому ключу без промежуточных списков.associateBy { it.name }преобразует список в map для быстрого доступа по ключу.- такой набор операций покрывает частые сценарии аналитики и индексирования данных.
groupingBy + fold удобен, когда нужна одна агрегированная цифра на ключ без списка промежуточных элементов.
Типичные ошибки
- Дважды пройти один и тот же
Sequence— послеtoList()исходная последовательность исчерпана; создайте новуюasSequence()или сохраните результат. - Итерировать
mutableListи одновременноremove—ConcurrentModificationException; копируйте список или удаляйте с конца по индексу. - Путать
ListиIntArray— у примитивного массива нетfilterиз stdlib без преобразования. - Держать sequence открытого файла вне
useLines— файл закроется, итерация сломается.
Дополнительный пример — безопасная обработка nullable-коллекций
Код ITЗагрузка примера кода…
Разбор:
- Вход: список тикетов содержит
null, повторы и потенциально "грязные" строки владельцев. - Ключевые API:
mapNotNull,map,filter,toSet,sorted. - Поток выполнения: сначала убираются
null, затем нормализуются строки, отбрасываются пустые, устраняются дубликаты и выполняется сортировка. - Результат: получается чистый, уникальный и отсортированный список владельцев.
- Типичная ошибка: использовать
map { it.owner }безmapNotNull; тогда придётся отдельно работать сString?и легко пропуститьnull-проверку.
Пример с Queue и Stack на ArrayDeque
val queue = ArrayDeque<Int>()
queue.addLast(10)
queue.addLast(20)
val first = queue.removeFirst() // 10
val stack = ArrayDeque<String>()
stack.addLast("A") // push
stack.addLast("B") // push
val top = stack.removeLast() // pop -> "B"
Разбор:
ArrayDeque<Int>()иArrayDeque<String>()создают двустороннюю очередь для разных типов данных.addLast/removeFirstреализуют FIFO-поведение очереди.addLast/removeLastреализуют LIFO-поведение стека.firstиtopсохраняют извлечённые значения для дальнейшей логики.- один контейнер закрывает два классических паттерна: queue и stack.
ArrayDeque в Kotlin закрывает большую часть задач, где раньше брали LinkedList только ради очереди или стека.