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

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

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

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

Коллекции и 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)

Неизменяемые 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}
ФункцияМожно менять на месте?
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

Оператор + для коллекций создаёт новый список. На тысячах элементов в цикле это дорого — тогда один раз buildList { }:

val ids = buildList {
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 }

Разбор выражения 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()

Пока не вызван 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()
}

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

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

data class Order(val region: String, val amount: Int)

val orders = listOf(
Order("EU", 100),
Order("EU", 50),
Order("US", 200)
)

val byRegion = orders.groupBy { it.region }
// Map: "EU" -> [Order(...), Order(...)], "US" -> [...]

val totalByRegion = orders.groupingBy { it.region }
.fold(0) { acc, order -> acc + order.amount }
// Map: "EU" -> 150, "US" -> 200

val userById = users.associateBy { it.name } // ключ — name

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


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

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

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


См. также

Другие статьи этого же раздела в боковом меню (как на странице «О разделе»).