Найти тему
Nuances of programming

Kotlin. Коллекции и последовательности

Оглавление

Источник: Nuances of Programming

Kotlin из коробки предоставляет два способа обработки данных: энергичный для Collection и ленивый для Sequence.

Collection и Sequence

Разница между ленивыми и энергичными вычислениями в том, когда они происходят. Коллекция трансформируется энергично. Каждая операция выполняется в момент вызова, а результат преобразования  —  новая коллекция. Преобразователи коллекций  —  это встраиваемые функции. Ниже встраиваемая функция map, создающая новый ArrayList:

public inline fun <T, R> Iterable<T>.map(transform: (T) -> R): List<R> { return mapTo(ArrayList<R>(collectionSizeOrDefault(10)), transform) }

Последовательности вычисляются лениво. То есть имеют два типа операций: промежуточные и терминальные (прерывающие). Промежуточные не выполняются сразу. Они сохраняются в список и запускаются цепочкой вместе с терминальной функцией для каждого элемента отдельно.

  • map, distinct, groupBy и т.д.  —  промежуточные, возвращают Sequence.
  • first, toList, count и т.д.  —  терминальные, не возвращают Sequence.

Последовательности не содержат ссылку на элементы коллекции. Они создаются на основе итератора оригинальной коллекции, и в них  —  ссылка на все промежуточные операции, которые должны быть выполнены. 

В отличие от трансформеров коллекций, трансформеры последовательностей не могут быть встраиваемыми функциями. Они не могут сохраняться, а последовательностям нужно именно это. Посмотрев на реализацию, мы видим, что преобразователь возвращает Sequence:

-2

Терминальные операции выполняются до совпадения с предикатом:

-3

В итераторе TransformingSequence из map применяется сохраненное преобразование:

-4

Стандартная библиотека предоставляет широкий набор функций для обоих типов контейнеров: find, filter, groupBy и многие другие. Проверьте список здесь, прежде чем писать собственные.

-5

Допустим, у нас есть список из объектов различных фигур. Мы хотим раскрасить фигуры жёлтым и получить первый квадрат. Давайте посмотрим, как и когда выполняется каждая операция для коллекции и последовательности.

-6

Коллекции

  • Вызывается map  —  создаётся новый ArrayList. Мы обрабатываем все элементы начальной коллекции и преобразуем её с копированием объектов. Меняем цвет и добавляем объект в новую коллекцию.
  • Вызывается first. Мы обрабатываем каждый элемент до тех пор, пока не найдём квадрат.

Последовательности

  • asSequence  —  последовательность создаётся на основе итератора оригинальной коллекции.
  • map. Трансформация добавляется в список выполняемых операций, но не выполняется.
  • first. Это терминальная операция. Значит, выполняются операции из списка выше. Обрабатываем каждый элемент начальной последовательности map и сразу же first. Условие из first выполняется уже на втором элементе. Не нужно обрабатывать всю последовательность!

Итак, когда мы работаем с последовательностями, промежуточные коллекции не создаются. Обработка элементов происходит один за одним. Значит, отображение выполняется только для некоторых элементов.

Ленивый и энергичный подходы
Ленивый и энергичный подходы

Порядок преобразований

Используете вы коллекции или последовательности, порядок преобразований имеет значение. В примере выше first может не вызываться для всего списка строго после map: это не имеет значения для map. Если мы сначала вызовем коллекцию, а затем преобразуем результат, то создадим только один новый объект  —  жёлтый квадрат. С последовательности мы не создаём два новых объекта. С коллекциями  —  не создаём список.

Избегайте лишней работы
Избегайте лишней работы

Терминальные операции могут закончить обработку раньше, а промежуточные последовательности вычисляются лениво. Значит, последовательности могут помочь избежать лишней работы. Всегда проверяйте порядок трансформаций!

Встраивание и большие массивы данных

Операции в коллекции используют встраиваемые функции, так что байткод самих операций и лямбда‐выражений, переданных как параметры, будет встроен. Коллекции создают новый список для каждого преобразования. Последовательности же содержат ссылку на функции преобразования.

Когда мы работаем с небольшим коллекциями и одной-двумя операциями, разница в производительности не существенна. Можно использовать коллекции. Но, когда вы работаете с большими списками, они могут стать дорогостоящими. Используйте последовательности.

К сожалению, я не знаю ни одного проведённого бенчмарка, который помог бы лучше понять, как производительность коллекций и последовательностей зависит от их размеров и цепочек операций.

В зависимости от размера данных выберите подходящий контейнер: коллекции  —  для небольших списков, последовательности  —  для больших. И помните о порядке преобразований.

Читайте также:

Читайте нас в телеграмме и vk

Перевод статьи: Florina Muntenescu: Collections and sequences in Kotlin