Коллекции это половина повседневного Kotlin-кода. На собесе проверяют три вещи: разницу read-only и mutable, знание операторов (map, groupBy, fold, associate) и понимание, когда нужна Sequence.
Read-only не значит immutable
val list: List<Int> = listOf(1, 2, 3) // read-only интерфейс
val mutable: MutableList<Int> = mutableListOf(1, 2)
mutable.add(3)
List в Kotlin это интерфейс только для чтения: у него нет add. Но это не гарантия неизменяемости. Под ним может лежать ArrayList, который кто-то меняет через ссылку MutableList, или его можно привести обратно as MutableList. На JVM List и MutableList компилируются в один и тот же java.util.List. Настоящие неизменяемые коллекции дают библиотека kotlinx.collections.immutable (persistentListOf) или копия при отдаче наружу.
Иерархия: Iterable → Collection → List / Set; Map отдельно. У каждого есть Mutable-версия.
Фабрики: listOf, mutableListOf, arrayListOf, setOf (сохраняет порядок вставки, это LinkedHashSet), hashSetOf, mapOf (тоже LinkedHashMap), buildList { add(1) }, List(5) { it * it }.
Основные операции
val orders = listOf(Order("Аня", 500), Order("Борис", 300), Order("Аня", 200))
orders.map { it.sum } // [500, 300, 200]
orders.filter { it.sum > 250 }
orders.sumOf { it.sum } // 1000
orders.groupBy { it.user } // Map<String, List<Order>>
orders.groupingBy { it.user }.eachCount() // {Аня=2, Борис=1}
orders.associateBy { it.user } // последний побеждает: Аня -> Order(Аня, 200)
orders.associate { it.user to it.sum } // Map из пар
orders.partition { it.sum > 250 } // Pair(подходящие, остальные)
orders.sortedByDescending { it.sum }
orders.maxByOrNull { it.sum }
orders.distinctBy { it.user }
orders.chunked(2); orders.windowed(2)
listOf(listOf(1), listOf(2, 3)).flatten()
orders.flatMap { listOf(it.user, it.user) }
listOf(1, 2, 3).zip(listOf("a", "b")) // [(1, a), (2, b)]
fold(initial) { acc, x -> } сворачивает коллекцию в одно значение с начальным аккумулятором. reduce берёт первый элемент как начальный и падает на пустой коллекции.
Для Map: getOrDefault, getOrPut (для MutableMap), mapValues, filterKeys, withDefault. Обращение map[key] возвращает nullable, getValue(key) бросает исключение при отсутствии ключа.
Жадность и Sequence
Операции над Iterable жадные: каждая создаёт новый список целиком.
val result = (1..1_000_000)
.map { it * 2 } // список на миллион
.filter { it % 3 == 0 } // ещё один список
.first()
Sequence ленивая: элементы проходят цепочку по одному, промежуточных коллекций нет, и обработка останавливается, как только результат получен.
val result = (1..1_000_000).asSequence()
.map { it * 2 }
.filter { it % 3 == 0 }
.first() // обработано всего 3 элемента
Операции делятся на промежуточные (map, filter, take, возвращают Sequence) и терминальные (toList, first, sum, forEach). Без терминальной ничего не выполнится.
Когда брать Sequence: длинные цепочки на больших коллекциях, ранний выход (first, take), бесконечные последовательности (generateSequence). Когда не брать: маленькие коллекции и одна-две операции, там список быстрее (у последовательности накладные расходы на итераторы и лямбды). sorted на последовательности всё равно соберёт всё в память.
Это аналог Java Stream, но однопоточный и без parallel().
Массивы
Array<Int> хранит объекты Integer, IntArray примитивы int[]. Для производительных задач нужны примитивные массивы. Сравнение массивов через == сравнивает ссылки; по содержимому contentEquals.
Частые ошибки
- Считать
Listнеизменяемым. associateByпри дублях ключей молча оставляет последний элемент.first()иreduceна пустой коллекции бросают исключение; безопасноfirstOrNull(),reduceOrNull.- Изменение коллекции во время обхода
forдаётConcurrentModificationException; удалять черезremoveAll { }илиiterator.remove(). asSequence()на списке из 10 элементов ради «оптимизации».
Что спросят на собесе
- Чем
Listотличается отMutableListи неизменяемого списка? - Чем
Sequenceотличается отIterableи когда её использовать? mapпротивflatMap,foldпротивreduce,groupByпротивassociateBy.- В каком порядке выполнятся
mapиfilterдля списка и для последовательности?