Коллекции и последовательности
API коллекций стандартной библиотеки — преобразования map, filter и flatMap, энергичный List против ленивой Sequence, read-only и изменяемые коллекции в рантайме, fold против reduce, groupBy против associateBy, массивы и примитивные IntArray.
9 вопросов
JuniorТеорияОчень частоЧем Array<T>, List<T> и IntArray различаются между собой в Kotlin?
Чем Array<T>, List<T> и IntArray различаются между собой в Kotlin?
Array<T> — JVM-массив объектов фиксированного размера, чьи ячейки можно переприсваивать. List<T> — интерфейс, за которым обычно стоит ArrayList, и он умеет расти. IntArray — примитивный int[] фиксированного размера, его элементы не боксируются.
Типичные ошибки
- ✗Пытаться сделать
addнаArray<T>, размер которого фиксирован при создании - ✗Считать, что
IntArrayбоксирует элементы так же, какArray<Int> - ✗Брать
Array<Int>в горячем числовом коде, где местоIntArray
Уточняющие вопросы
- →Когда вы осознанно выберете
Array<T>вместоList<T>? - →Во что обходится вызов
toIntArray()наList<Int>?
JuniorТеорияОчень частоЧем map отличается от flatMap у коллекции в Kotlin?
Чем map отличается от flatMap у коллекции в Kotlin?
map применяет преобразование к каждому элементу и отдаёт ровно один результат на элемент, поэтому размер не меняется. flatMap ждёт от преобразования коллекцию на каждый элемент и склеивает их все в один плоский список, так что размер результата меняется.
Типичные ошибки
- ✗Звать
map, когда преобразование возвращает коллекцию, и получать список списков - ✗Ждать, что
flatMapсохранит размер исходного списка - ✗Думать, что
flatMapпопутно убирает дубликаты или null
Уточняющие вопросы
- →Как развернуть готовый
List<List<T>>, когда преобразовывать нечего? - →Что даёт
mapNotNullпо сравнению сmapи последующимfilterNotNull?
JuniorТеорияЧастоЧем List отличается от MutableList в рантайме?
Чем List отличается от MutableList в рантайме?
Никакой: различие — это контракт времени компиляции. Обычно за обоими стоит один и тот же java.util.ArrayList, а List — интерфейс без изменяющих методов. Поэтому ваш List всё ещё может измениться, если кто-то держит на него изменяемую ссылку.
Типичные ошибки
- ✗Считать, что
Listотдаёт вам защитную копию элементов - ✗Полагать, что полученный
Listуже никогда не изменится - ✗Думать, что режим только на чтение держится на
UnsupportedOperationException
Уточняющие вопросы
- →Как отдать список, который действительно не изменится под вызывающим кодом?
- →Почему
List, пришедший из Java-кода, всё равно можно изменить?
JuniorТеорияЧастоКак Sequence вычисляет свои операторы по сравнению с List?
Как Sequence вычисляет свои операторы по сравнению с List?
Цепочка над List энергичная: каждый оператор проходит всю коллекцию и выделяет новый промежуточный список. Sequence ленива: ничего не выполняется до терминального оператора, а затем каждый элемент по одному проталкивается через всю цепочку.
Типичные ошибки
- ✗Думать, что один
asSequence()запускает работу без терминального оператора - ✗Считать, что цепочка над
Listсплавляется в один проход - ✗Принимать
Sequenceза параллельную или конкурентную конструкцию
Уточняющие вопросы
- →Какие терминальные операторы могут оборвать цепочку
Sequenceраньше времени и почему? - →Сколько промежуточных списков выделит цепочка из трёх операторов над
List?
MiddleТеорияЧастоЧем associateBy отличается от groupBy для списка?
Чем associateBy отличается от groupBy для списка?
groupBy отдаёт Map<K, List<V>> и сохраняет под ключом каждый элемент. associateBy отдаёт Map<K, V> с одним значением на ключ, поэтому при совпадении ключей последний элемент молча затирает предыдущий. Берите его только при уникальном ключе.
Типичные ошибки
- ✗Применять
associateByк неуникальному ключу и тихо терять элементы - ✗Ждать, что
associateByгромко упадёт при повторе ключа - ✗Думать, что
groupByоставляет по одному элементу на ключ
Уточняющие вопросы
- →Чем
associateотличается отassociateByпо тому, что возвращает лямбда? - →Какой оператор посчитает число элементов на ключ за один проход?
MiddleКодЧастоПосчитайте суммарную длину имён в списке, возвращая 0 для пустого списка
Посчитайте суммарную длину имён в списке, возвращая 0 для пустого списка
У reduce нет затравки: он стартует с первого элемента, на пустой коллекции бросает исключение, а тип аккумулятора равен типу элемента. fold берёт начальное значение, поэтому переживает пустой список и меняет тип: names.fold(0) { acc, n -> acc + n.length }.
Типичные ошибки
- ✗Брать
reduce, когда тип результата отличается от типа элемента - ✗Забывать, что
reduceбросаетUnsupportedOperationExceptionна пустой коллекции - ✗Ставить проверку
isEmpty()вместо того, чтобы задать затравку аккумулятора
Уточняющие вопросы
- →Когда здесь лучше подойдёт
runningFold, а неfold? - →Как
reduceOrNullменяет поведение на пустой коллекции?
MiddleПроизводительностьИногдаКогда перевод цепочки над коллекцией в Sequence действительно окупается?
Когда перевод цепочки над коллекцией в Sequence действительно окупается?
Окупается, когда данных много или цепочка длинная: Sequence не выделяет промежуточных списков, а терминальный first() или take(n) обрывает работу раньше. На маленькой коллекции она проигрывает: механика итератора дороже недолговечных списков.
Типичные ошибки
- ✗Оборачивать список из трёх элементов в
asSequence()и ждать ускорения - ✗Считать, что
Sequenceраскидывает работу по потокам - ✗Забывать, что цепочка над
Listвыделяет новый список на каждый оператор
Уточняющие вопросы
- →Сколько промежуточных списков выделит
list.map { }.filter { }.take(5)? - →Стали бы вы решать этот спор через JVM-бенчмарк-харнесс
JMHи почему?
SeniorТеорияИногдаЧем Array<Int>, IntArray и Java-массив int[] различаются на JVM?
Чем Array<Int>, IntArray и Java-массив int[] различаются на JVM?
IntArray компилируется ровно в Java-массив int[]: непрерывный блок примитивов без объектов-обёрток. Array<Int> компилируется в Integer[], поэтому каждый элемент — отдельный боксированный объект со своим заголовком и разыменованием на каждом чтении.
Типичные ошибки
- ✗Объявлять
Array<Int>в горячем числовом коде и платить за боксинг - ✗Считать, что у
Array<Int>иIntArrayодно представление на JVM - ✗Ждать, что JIT уберёт боксинг из
Integer[]
Уточняющие вопросы
- →Какой вызов stdlib превращает
List<Int>вIntArrayи во что это обходится? - →Почему обобщённая функция, принимающая
Array<T>, не приметIntArray?
SeniorТеорияИногдаПочему map { }.filter { }.first() делает намного меньше работы на Sequence, чем на List?
Почему map { }.filter { }.first() делает намного меньше работы на Sequence, чем на List?
На List каждый оператор отрабатывает по всем элементам целиком, поэтому map и filter срабатывают по n раз, и строятся два промежуточных списка. На Sequence элементы идут по цепочке по одному, и first() останавливается на первом совпадении.
Типичные ошибки
- ✗Считать, что компилятор сплавляет цепочку над
Listв один проход - ✗Ждать, что
first()оборвёт ранние операторы цепочки надList - ✗Думать, что
Sequenceвсё равно материализует отфильтрованный результат доfirst()
Уточняющие вопросы
- →Как меняется картина, если терминальный оператор —
toList()? - →При каком размере коллекции
asSequence()здесь перестаёт себя окупать?