Перейти к основному содержимому

Коллекции и Sequence в Kotlin

Разработчику Архитектору

Дальше: Типы данных и переменные · Справочник Kotlin

Сначала — теория (раздел 3 "Данные")
список.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)
DequeaddFirst/addLast, removeFirst/removeLast

Неизменяемые listOf / mapOf / setOf поддерживают чтение; "изменение" — новая коллекция (+, buildList, копия в mutable*).


Когда какой тип выбрать

ЗадачаТип
Список задач по порядкуList
Уникальные теги, посещённые idSet
Поиск по ключу (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/mapSequence
Когда считаетсяНа каждом шаге сразуПри терминальном операторе
ПамятьПромежуточные спискиОдин элемент за раз
Когда выбиратьДо ~сотен–тысяч элементов, короткая цепочкаДлинная цепочка, большой файл, генератор

Группировка и Map из списка

Код ITЗагрузка примера кода…

Разбор:

  • groupBy { it.region } формирует Map<String, List<Order>> с группами по региону.
  • groupingBy { ... }.fold(0) { ... } сразу агрегирует сумму по каждому ключу без промежуточных списков.
  • associateBy { it.name } преобразует список в map для быстрого доступа по ключу.
  • такой набор операций покрывает частые сценарии аналитики и индексирования данных.

groupingBy + fold удобен, когда нужна одна агрегированная цифра на ключ без списка промежуточных элементов.


Типичные ошибки

  1. Дважды пройти один и тот же Sequence — после toList() исходная последовательность исчерпана; создайте новую asSequence() или сохраните результат.
  2. Итерировать mutableList и одновременно removeConcurrentModificationException; копируйте список или удаляйте с конца по индексу.
  3. Путать List и IntArray — у примитивного массива нет filter из stdlib без преобразования.
  4. Держать 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 только ради очереди или стека.


Связанные материалы