Kotlin guide done. (20.10.26)

This commit is contained in:
2026-10-20 12:54:25 +03:00
parent da3ad9dd1a
commit 6853ada348
81 changed files with 30112 additions and 0 deletions
+368
View File
@@ -0,0 +1,368 @@
---
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)