Files
obsidian/Kotlin/Коллекции/Sequence.md
T

369 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Sequence
description: Ленивые вычисления, порядок обхода элементов и когда Sequence выигрывает у List
tags:
- kotlin
- collections
---
`Sequence<T>` - ленивый аналог коллекции. Операции не выполняются до запроса результата, и элементы проходят всю цепочку по одному.
```kotlin
fun main() {
val list = listOf(1, 2, 3, 4, 5)
println("-- List:")
val eager = list
.map { println(" map $it"); it * 2 }
.filter { println(" filter $it"); it > 4 }
.first()
println(" результат $eager")
println("-- Sequence:")
val lazy = list.asSequence()
.map { println(" map $it"); it * 2 }
.filter { println(" filter $it"); it > 4 }
.first()
println(" результат $lazy")
}
```
`List` выполнит 5 `map` и 5 `filter` - всего 10 операций, создав два промежуточных списка. `Sequence` выполнит 3 `map` и 3 `filter` и остановится, как только найдёт первый подходящий элемент.
## Два порядка обхода
Это ключ к пониманию `Sequence`:
```mermaid
graph LR
subgraph List["List: по операциям"]
L1["map всех 5"] --> L2["filter всех 5"] --> L3["first"]
end
subgraph Seq["Sequence: по элементам"]
S1["1: map, filter"] --> S2["2: map, filter"] --> S3["3: map, filter → стоп"]
end
```
| | `List` | `Sequence` |
|---|---|---|
| порядок | операция за операцией | элемент за элементом |
| промежуточные коллекции | на каждом шаге | нет |
| вычисление | сразу | при терминальной операции |
| можно ли прервать | нет | да |
| бесконечные данные | нет | да |
## Создание
```kotlin
fun main() {
println(listOf(1, 2, 3).asSequence().toList()) // [1, 2, 3]
println(sequenceOf(1, 2, 3).toList()) // [1, 2, 3]
println(emptySequence<Int>().toList()) // []
println(generateSequence(1) { it * 2 }.take(5).toList()) // [1, 2, 4, 8, 16]
println(generateSequence(1) { if (it < 5) it + 1 else null }.toList()) // [1, 2, 3, 4, 5]
val built = sequence {
yield(1)
yieldAll(listOf(2, 3))
yield(4)
}
println(built.toList()) // [1, 2, 3, 4]
}
```
`generateSequence` с `null` в лямбде - способ задать конец. `sequence { }` - билдер с `yield`, внутри него можно писать любую логику, включая циклы.
## Промежуточные и терминальные операции
```kotlin
fun main() {
val seq = sequenceOf(1, 2, 3)
// промежуточные: возвращают Sequence, ничего не считают
val chain = seq.map { it * 2 }.filter { it > 2 }.take(2)
println("цепочка построена, вычислений не было")
// терминальные: запускают вычисление
println(chain.toList()) // [4, 6]
println(seq.sum()) // 6
println(seq.count()) // 3
println(seq.first()) // 1
println(seq.any { it > 2 }) // true
}
```
| Промежуточные | Терминальные |
|---|---|
| `map`, `filter`, `flatMap` | `toList`, `toSet`, `toMap` |
| `take`, `drop`, `distinct` | `first`, `last`, `single` |
| `sorted`, `chunked`, `windowed` | `sum`, `count`, `average`, `max` |
| `onEach`, `mapNotNull` | `any`, `all`, `none`, `forEach` |
| `zip`, `plus` | `fold`, `reduce`, `joinToString` |
## Бесконечные последовательности
То, чего `List` не умеет в принципе:
```kotlin
fun main() {
val naturals = generateSequence(1) { it + 1 }
println(naturals.take(5).toList()) // [1, 2, 3, 4, 5]
println(naturals.filter { it % 7 == 0 }.first()) // 7
println(naturals.map { it * it }.takeWhile { it < 50 }.toList()) // [1, 4, 9, 16, 25, 36, 49]
val fib = generateSequence(0 to 1) { (a, b) -> b to (a + b) }.map { it.first }
println(fib.take(10).toList()) // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
}
```
Важно: терминальная операция без ограничения на бесконечной последовательности зависнет. `naturals.toList()` не завершится никогда.
## Sequence в цепочке с ранним выходом
```kotlin
data class Record3(val id: Int, val active: Boolean, val score: Int)
fun main() {
val records = List(1_000_000) { Record3(it, it % 100 == 0, it % 7) }
val found = records.asSequence()
.filter { it.active }
.map { it.score * 2 }
.firstOrNull { it > 10 }
println(found) // 12
val top3 = records.asSequence()
.filter { it.active }
.take(3)
.map { it.id }
.toList()
println(top3) // [0, 100, 200]
}
```
Здесь `Sequence` обрабатывает несколько сотен элементов вместо миллиона. На `List` каждый `filter`/`map` прошёл бы весь миллион и создал два списка.
## Когда Sequence не выигрывает
```kotlin
fun main() {
val small = listOf(1, 2, 3, 4, 5)
// Sequence здесь только добавляет накладные расходы
println(small.map { it * 2 }.sum()) // 30
println(small.asSequence().map { it * 2 }.sum()) // 30, но медленнее
}
```
`Sequence` создаёт объект-обёртку на каждую операцию и вызывает лямбды через интерфейс итератора. На маленьких коллекциях и коротких цепочках это дороже, чем прямой проход.
Практическое правило:
| Условие | Выбор |
|---|---|
| меньше ~1000 элементов, 1-2 операции | `List` |
| много элементов и 3+ операций | `Sequence` |
| есть `first`, `find`, `take`, `any` после цепочки | `Sequence` |
| источник бесконечный или потоковый (файл, БД) | `Sequence` |
| нужен результат целиком, операций мало | `List` |
| нужен индексный доступ к результату | `List` |
## Операции, которые ломают ленивость
Некоторые операции обязаны прочитать всё, прежде чем выдать первый элемент:
```kotlin
fun main() {
val seq = sequenceOf(3, 1, 2)
.onEach { println("читаю $it") }
.sorted() // нужны все элементы
println("до first ничего не прочитано")
println(seq.first()) // здесь прочитаются все три
}
```
Такие операции - `sorted`, `sortedBy`, `distinct` (частично), `groupBy`, `chunked`. Ставьте их как можно позже в цепочке, а `filter` - как можно раньше.
## Sequence одноразов
```kotlin
fun main() {
val seq = listOf(1, 2, 3).asSequence().map { it * 2 }
println(seq.toList()) // [2, 4, 6]
println(seq.toList()) // [2, 4, 6] — здесь повезло: источник List
val once = sequence { yield(1); yield(2) }
println(once.toList()) // [1, 2]
println(once.toList()) // [1, 2] — тоже работает, лямбда вызвана заново
val fromIterator = listOf(1, 2, 3).iterator().asSequence()
println(fromIterator.toList()) // [1, 2, 3]
try {
fromIterator.toList()
} catch (e: IllegalStateException) {
println(e.message) // This sequence can be consumed only once.
}
}
```
Последовательность из итератора обходится один раз: `Iterator.asSequence()` оборачивает результат в `constrainOnce()`, который фиксирует это ограничение и бросает `IllegalStateException` при повторной попытке. Тот же механизм доступен явно:
```kotlin
fun main() {
val seq = sequenceOf(1, 2).constrainOnce()
println(seq.toList()) // [1, 2]
try {
seq.toList()
} catch (e: IllegalStateException) {
println("повторный обход запрещён")
}
}
```
## Чтение файла
Главный практический случай: данные, которые не влезают в память.
```kotlin
import java.io.File
fun main() {
val tmp = File.createTempFile("demo", ".txt")
tmp.writeText((1..100).joinToString("\n"))
val sum = tmp.useLines { lines -> // Sequence<String>
lines.map { it.toInt() }.filter { it % 2 == 0 }.sum()
}
println(sum) // 2550
val firstBig = tmp.useLines { lines -> lines.first { it.toInt() > 50 } }
println(firstBig) // 51 — файл прочитан до 51-й строки, не целиком
tmp.delete()
}
```
`useLines` даёт `Sequence<String>` и закрывает файл после работы. Альтернатива `readLines()` загрузила бы весь файл в `List<String>`.
## Sequence и Flow
`Sequence` - ленивый, но **синхронный**: получение следующего элемента блокирует поток. Для асинхронных источников нужен `Flow`:
| | `Sequence` | `Flow` |
|---|---|---|
| вычисление | лениво | лениво |
| блокировка | да | нет, `suspend` |
| внутри можно `delay` | нет | да |
| источник | память, файл, генератор | сеть, БД, события |
Подробнее: [Flow](../Корутины/Flow.md)
> [!info] Отличия от Java
> | Java Stream | Kotlin Sequence |
> |---|---|
> | `list.stream()` | `list.asSequence()` |
> | `Stream.of(1, 2)` | `sequenceOf(1, 2)` |
> | `Stream.iterate(1, f)` | `generateSequence(1, f)` |
> | `collect(toList())` | `toList()` |
> | `.parallel()` | **нет аналога** |
> | `IntStream`, `LongStream` | нет, только `Sequence<T>` |
> | `Files.lines(path)` | `file.useLines { }` |
> | нет генератора с `yield` | `sequence { yield(x) }` |
>
> **`Sequence` и `Stream` - почти одно и то же.** Ленивость, промежуточные и терминальные операции, короткое замыкание на `findFirst`/`first` - всё совпадает. Поэтому опыт работы со стримами переносится напрямую, и наоборот: если в Java вы написали бы `.stream()`, в Kotlin нужен `.asSequence()`.
>
> **Главное отличие - нет параллелизма.** `Stream.parallel()` распределяет работу по `ForkJoinPool`; у `Sequence` такого нет вообще. Причина в том, что для конкурентности в Kotlin предусмотрены корутины, а не параллельные стримы. Если нужна параллельная обработка - либо используйте Java-стримы напрямую (они доступны), либо корутины:
> ```kotlin
> fun main() {
> // Java Stream доступен из Kotlin
> val sum = listOf(1, 2, 3).stream().parallel().mapToInt { it * 2 }.sum()
> println(sum) // 12
> }
> ```
>
> **Нет специализаций для примитивов.** В Java `IntStream` избегает упаковки: `list.stream().mapToInt(x -> x).sum()` работает с `int`. `Sequence<Int>` упаковывает каждый элемент в `Integer`. На числовых расчётах по большим данным Java-стримы могут оказаться быстрее.
>
> **`sequence { yield(x) }` - генератор, которого в Java нет.** Он реализован на том же механизме приостановки, что и корутины, поэтому внутри можно писать обычные циклы и условия:
> ```kotlin
> fun main() {
> val primes = sequence {
> var n = 2
> while (true) {
> if ((2 until n).none { n % it == 0 }) yield(n)
> n++
> }
> }
> println(primes.take(5).toList()) // [2, 3, 5, 7, 11]
> }
> ```
> В Java это требует либо отдельного потока, либо ручной реализации `Iterator` с сохранением состояния.
>
> Сравнить: [HashMap в Java](../../Программирование%20на%20Java/Коллекции/HashMap.md)
> [!warning] Подводные камни
> **Без терминальной операции не произойдёт ничего:**
> ```kotlin
> fun main() {
> listOf(1, 2, 3).asSequence().map { println("это не выведется"); it }
> println("конец") // только "конец"
> }
> ```
> Это частая ошибка при замене `forEach` на цепочку: если забыть терминальную операцию, код просто не выполнится, и компилятор не предупредит.
>
> **`Sequence` на маленьких коллекциях медленнее.** Обёртки и вызовы через итератор стоят дороже прямого цикла. Для списка из 10 элементов `asSequence()` - пессимизация.
>
> **Терминальная операция на бесконечной последовательности зависает:**
> ```kotlin
> fun main() {
> val naturals = generateSequence(1) { it + 1 }
> println(naturals.take(3).toList()) // [1, 2, 3] — ок
> // naturals.toList() // зависнет навсегда
> // naturals.count() // тоже
> // naturals.filter { it > 10 }.toList() // и это
> }
> ```
> Любая цепочка из бесконечного источника обязана содержать `take`, `takeWhile`, `first` или `find`.
>
> **`sorted` и `groupBy` убивают ленивость** - они читают всё до первого результата. Цепочка `asSequence().sorted().first()` не быстрее, чем на `List`.
>
> **Последовательность из итератора обходится один раз** - второй вызов падает:
> ```kotlin
> fun main() {
> val seq = listOf(1, 2).iterator().asSequence()
> println(seq.count()) // 2
> try {
> seq.count()
> } catch (e: IllegalStateException) {
> println("повторный обход: ${e.message}")
> }
> }
> ```
> Исключение здесь лучше молчаливого пустого результата, но возникает оно в рантайме и только на втором обходе - в редкой ветке кода его легко не заметить. Если последовательность может понадобиться дважды, сохраните `toList()`.
>
> **Побочные эффекты в промежуточных операциях выполняются в неожиданном порядке:**
> ```kotlin
> fun main() {
> val log = mutableListOf<String>()
> sequenceOf(1, 2)
> .onEach { log.add("A$it") }
> .onEach { log.add("B$it") }
> .toList()
> println(log) // [A1, B1, A2, B2] — не [A1, A2, B1, B2]
> }
> ```
> На `List` вывод был бы `[A1, A2, B1, B2]`. Код, зависящий от порядка побочных эффектов, при переходе на `Sequence` сломается.
>
> **Повторный обход пересчитывает всё заново:**
> ```kotlin
> fun main() {
> var calls = 0
> val seq = listOf(1, 2, 3).asSequence().map { calls++; it * 2 }
> seq.toList()
> seq.toList()
> println(calls) // 6 — лямбда вызвана дважды для каждого элемента
> }
> ```
> Если результат нужен несколько раз, сохраните его в `List`.
>
> **`Sequence` синхронна.** Внутри её лямбд нельзя вызывать `suspend`-функции - для этого существует `Flow`. Попытка использовать `Sequence` для сетевых запросов приведёт к блокировке потока.
>
> **`useLines` закрывает файл по выходу из лямбды.** Вернуть `Sequence` наружу нельзя - она окажется на закрытом ресурсе:
> ```kotlin
> import java.io.File
>
> fun main() {
> val tmp = File.createTempFile("leak", ".txt")
> tmp.writeText("1\n2")
> val leaked = tmp.useLines { it } // Sequence на закрытом файле
> try {
> println(leaked.toList())
> } catch (e: java.io.IOException) {
> println("поток закрыт: ${e.message}") // поток закрыт: Stream closed
> }
> tmp.delete()
> }
> ```
> Внутри лямбды нужно получить готовое значение: `useLines { it.toList() }` или сразу агрегат.
Далее: [Коллекции под капотом](Коллекции%20под%20капотом.md)