Что такое sequence и когда его лучше использовать?

В Kotlin Sequence — это специальный тип коллекции, предназначенный для ленивой обработки данных. Он позволяет строить цепочки операций (map, filter, take, drop, и т.д.) без немедленного выполнения. В отличие от обычных коллекций, таких как List и Set, операции над Sequence не выполняются сразу, а откладываются до тех пор, пока не будет вызвана терминальная операция, например toList() или count().

Принцип работы Sequence

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

val result = list
.map { it \* 2 } // создаётся новый список
.filter { it > 10 } // ещё один новый список
.take(5) // снова новый список

В этом примере будут созданы как минимум три промежуточные коллекции, что может быть ресурсоёмко при больших объёмах данных.

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

val result = list.asSequence()
.map { it \* 2 }
.filter { it > 10 }
.take(5)
.toList()

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

Терминальные и промежуточные операции

  • Промежуточные операции возвращают новый Sequence и не выполняются сразу:

    • map
    • filter
    • flatMap
    • take
    • drop
    • onEach
  • Терминальные операции запускают выполнение всей цепочки:

    • toList(), toSet(), sum(), count(), first(), last(), find(), forEach() и т.д.

Пример поведения Sequence с отладкой

val list = listOf(1, 2, 3, 4, 5)
val result = list.asSequence()
.map {
println("map($it)")
it \* 2
}
.filter {
println("filter($it)")
it > 5
}
.take(2)
.toList()

Вывод в консоли:

map(1)
filter(2)
map(2)
filter(4)
map(3)
filter(6)
map(4)
filter(8)

Как только take(2) находит два подходящих элемента, вычисления прекращаются — оставшиеся элементы не обрабатываются.

Когда лучше использовать Sequence

1. Большие коллекции

Когда работаешь с миллионами элементов — обычные map и filter создадут кучу промежуточных списков. Sequence помогает избежать этого и обрабатывать всё на лету.

2. Бесконечные источники данных

С помощью generateSequence() или sequence {} можно создать бесконечный поток данных:

val evens = generateSequence(0) { it + 2 }
val firstTen = evens.take(10).toList() // работает, даже несмотря на бесконечность

3. Работа с IO (например, файлы)

При чтении строк из файла удобнее использовать BufferedReader.lineSequence(), чтобы не грузить весь файл в память:

File("log.txt").bufferedReader().lineSequence()
.filter { it.contains("ERROR") }
.map { it.substringAfter("ERROR") }
.toList()

4. Частичная обработка

Если нужен только первый подходящий элемент — Sequence даёт преимущество, так как не обрабатывает весь список:

val result = list.asSequence()
.filter { expensiveCheck(it) }
.firstOrNull()

5. Комбинация трансформаций

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

Создание последовательностей

Через .asSequence()

Обычный способ начать работу с Sequence:

val seq = listOf(1, 2, 3).asSequence()

Через generateSequence

Генератор последовательности:

val fib = generateSequence(Pair(0, 1)) { Pair(it.second, it.first + it.second) }
.map { it.first }
.take(10)
.toList() // \[0, 1, 1, 2, 3, 5, 8, 13, 21, 34\]

Через sequence {} (блок-генератор)

val seq = sequence {
yield(1)
yieldAll(listOf(2, 3, 4))
yield(5)
}

Отличие от обычных коллекций

Параметр List / Set / Map Sequence
Выполнение операций Немедленно (жадно) Отложенно (лениво)
--- --- ---
Промежуточные коллекции Создаются Не создаются
--- --- ---
Производительность Может быть ниже при длинных цепочках Лучше при длинных цепочках
--- --- ---
Возможность переиспользовать Можно вызывать повторно После терминальной операции нельзя
--- --- ---
Потоковая обработка Нет Да
--- --- ---
Поддержка бесконечности Нет Да
--- --- ---

Потенциальные недостатки

Одноразовость: Нельзя повторно использовать Sequence после завершения:

val seq = listOf(1, 2, 3).asSequence().map { it \* 2 }
seq.toList()
seq.toList() // Ошибка: уже потреблена
  • Не все операции эффективны: Например, sorted() требует полного обхода всей последовательности — теряется преимущество лени.

  • Не подходит для мутабельных структур: Sequence — это неизменяемая структура, в ней нельзя изменять элементы или добавлять новые напрямую.

  • Плохая читабельность при сложных цепочках: Иногда цепочка Sequence может быть сложной для отладки.

Особенности реализации

Kotlin реализует Sequence как обёртку над Iterator<T>:

interface Sequence&lt;out T&gt; {
operator fun iterator(): Iterator&lt;T&gt;
}

Когда ты вызываешь .asSequence(), Kotlin возвращает Sequence-обёртку, и по мере запроса данных из iterator() каждый элемент проходит через цепочку операций.

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