Коллекции и последовательности
Коллекции в Kotlin — это не новые структуры данных поверх JVM, а новый набор интерфейсов над старыми. За List<T>, MutableList<T> и ArrayList<T> в рантайме обычно стоит один и тот же java.util.ArrayList, а разница между первыми двумя существует только на этапе компиляции: List просто не объявляет изменяющих методов. Отсюда главный вывод темы, который проверяют на каждом втором собеседовании: read-only — это контракт, а не гарантия неизменности. Отданный наружу List спокойно поменяется, если кто-то держит на тот же объект ссылку типа MutableList.
Второй водораздел — когда выполняется цепочка операторов. На обычном Iterable она энергична: каждый оператор целиком проходит коллекцию и выделяет новый промежуточный список, поэтому list.map { }.filter { } — это два списка и два прохода по данным. Sequence переворачивает порядок вычисления: операторы лишь собирают конвейер обёрток, до терминальной операции не выполняется ничего, а затем элементы идут через всю цепочку по одному, вертикально. Отсюда и выигрыш (нет промежуточных списков, first() обрывает работу на первом совпадении), и ловушка (на маленькой коллекции механика итератора дороже недолговечных списков — Sequence там просто медленнее). Третья ось темы — представление на JVM: IntArray компилируется ровно в int[], а Array<Int> — в Integer[], где каждый элемент боксирован в отдельный объект. Разбор по слоям — ниже.
Карта темы
- Read-only и изменяемые коллекции — почему
Listне является неизменяемым, что происходит с ним на границе Java и как отдать наружу настоящий снимок данных. - Преобразования коллекций —
map,filterиflatMap, их влияние на размер результата и цена того, что каждый оператор материализует новый список. - fold против reduce — явная затравка против первого элемента, поведение на пустой коллекции и смена типа аккумулятора.
- Группировка и ассоциирование —
groupBy, сохраняющий все элементы,associateBy, у которого последний ключ молча затирает предыдущий, иgroupingByбез промежуточных списков. - Sequence и ленивые вычисления — конвейер обёрток, терминальные операции, короткое замыкание и точный ответ на вопрос, когда лень окупается, а когда вредит.
- Массивы и примитивные массивы —
Array<T>противIntArrayна JVM, инвариантность массивов в Kotlin, сравнение по содержимому иvararg.
Частые ошибки и ловушки
| Ошибка | Последствие |
|---|---|
Считать List неизменяемым типом рантайма | Это read-only представление: тот же объект меняется через любую сохранённую ссылку MutableList |
Считать, что List отдаёт вам защитную копию элементов | Копии нет — за List и MutableList стоит один и тот же объект; снимок делает только toList() |
Думать, что read-only держится на UnsupportedOperationException | Изменяющих методов у List просто нет в API — запрет проверяет компилятор, а не рантайм |
Забывать, что на JVM оба типа стираются в java.util.List | Java-код спокойно вызовет add() на вашем List, и Kotlin-сторона увидит изменение |
Звать map, когда преобразование возвращает коллекцию | На выходе List<List<T>> вместо плоского списка — здесь нужен flatMap |
Ждать, что flatMap сохранит размер исходного списка | Размер результата произвольный, а дубликаты и null он попутно не убирает |
Считать, что компилятор сплавляет цепочку над List в один проход | Никакого слияния: map{}.filter{} — два прохода и два выделенных списка |
Брать reduce, когда тип результата отличается от типа элемента | Аккумулятор reduce имеет тип элемента — сменить тип может только fold |
Забывать, что reduce бросает UnsupportedOperationException на пустой коллекции | Падение в проде на пустом списке; fold c затравкой или reduceOrNull его переживают |
Ставить проверку isEmpty() вместо затравки аккумулятора | Лишняя ветка там, где начальное значение fold само даёт результат для пустого случая |
Применять associateBy к неуникальному ключу | Последний элемент молча затирает предыдущий — данные теряются без единого исключения |
Ждать, что associateBy громко упадёт при повторе ключа | Он не падает никогда; уникальность ключа обязаны гарантировать вы |
Думать, что groupBy оставляет по одному элементу на ключ | groupBy возвращает Map<K, List<V>> и сохраняет все элементы группы |
Думать, что один asSequence() уже запускает работу | Без терминальной операции не выполнится ни одна лямбда — цепочка просто собрана |
Принимать Sequence за параллельную конструкцию | Это ленивый однопоточный конвейер; ни потоков, ни ядер он не использует |
Оборачивать список из трёх элементов в asSequence() ради скорости | На малых данных Sequence медленнее: операторы не встраиваются, каждый шаг — виртуальный вызов |
Пытаться сделать add на Array<T> | Размер массива фиксирован при создании — расти умеет List, а не массив |
Считать, что IntArray боксирует элементы так же, как Array<Int> | IntArray — это int[] без обёрток; Array<Int> — Integer[] с объектом на каждый элемент |
Сравнивать массивы через == | Это сравнение ссылок; по содержимому сравнивают contentEquals / contentDeepEquals |
Значение для собеседований
Тема выглядит джуниорской, а работает как фильтр: почти все знают, что делает map, и почти никто не может сказать, сколько списков выделит map{}.filter{}.first() и почему на Sequence это число равно нулю. Интервьюер проверяет здесь не словарь операторов, а модель исполнения — что физически лежит за List, в какой момент срабатывает лямбда и где именно данные могут молча потеряться. Три вопроса вытаскивают почти всё: «чем List отличается от MutableList в рантайме», «почему reduce падает на пустом списке» и «когда Sequence окупается».
Что обычно проверяют:
- Что различие
ListиMutableList— контракт компилятора, и что read-only не значит неизменяемый. - Разницу
mapиflatMapи то, что каждый оператор надIterableматериализует новый список. foldпротивreduce: затравка, тип аккумулятора и поведение на пустой коллекции.groupByпротивassociateByи тихую потерю элементов при повторе ключа.- Порядок вычисления в
Sequence, роль терминальной операции и короткое замыкание. - Когда ленивая цепочка окупается, а когда проигрывает энергичной.
Array<T>противIntArrayна JVM и почему массивы не сравнивают через==.
Типичный неверный ответ: «List — неизменяемая коллекция, MutableList — изменяемая». Это звучит уверенно и ведёт прямо к багу: кандидат отдаёт наружу List, полученный простым присваиванием из MutableList, считает данные защищёнными — и получает коллекцию, которая меняется под вызывающим кодом. На деле List — это read-only интерфейс над тем же объектом, а настоящий снимок даёт только копирование (toList()). Второй классический провал — «Sequence всегда быстрее»: на коллекции из десятка элементов ленивая цепочка проигрывает, потому что операторы над Iterable встраиваемые (inline), а каждый шаг Sequence — объект-обёртка с виртуальными вызовами на каждый элемент.