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
+365
View File
@@ -0,0 +1,365 @@
---
title: List, Set, Map
description: Три семейства коллекций, их гарантии, стоимость операций и выбор между ними
tags:
- kotlin
- collections
---
| | `List` | `Set` | `Map` |
|---|---|---|---|
| порядок | по индексу | вставки (у `LinkedHashSet`) | вставки (у `LinkedHashMap`) |
| дубликаты | разрешены | запрещены | ключи уникальны |
| доступ | по индексу, O(1) | только поиск, O(1) | по ключу, O(1) |
| поиск элемента | O(n) | O(1) | O(1) по ключу |
| основной вопрос | «что по порядку» | «есть ли такой» | «что соответствует» |
## List
```kotlin
fun main() {
val list = listOf("a", "b", "c", "b")
println(list[1]) // b — доступ по индексу
println(list.getOrNull(10)) // null
println(list.getOrElse(10) { "?" }) // ?
println(list.indexOf("b")) // 1 — первое вхождение
println(list.lastIndexOf("b")) // 3
println(list.subList(1, 3)) // [b, c]
println(list.size) // 4 — дубликат сохранён
}
```
Доступ к краям и срезы:
```kotlin
fun main() {
val list = listOf(1, 2, 3, 4, 5)
println("${list.first()} ${list.last()}") // 1 5
println(list.firstOrNull { it > 3 }) // 4
println(list.take(2)) // [1, 2]
println(list.drop(2)) // [3, 4, 5]
println(list.takeLast(2)) // [4, 5]
println(list.slice(1..3)) // [2, 3, 4]
println(list.chunked(2)) // [[1, 2], [3, 4], [5]]
println(list.windowed(2)) // [[1, 2], [2, 3], [3, 4], [4, 5]]
println(list.reversed()) // [5, 4, 3, 2, 1]
}
```
Изменение через `MutableList`:
```kotlin
fun main() {
val list = mutableListOf(1, 2, 3)
list.add(4)
list.add(0, 0) // по индексу
list[1] = 10
list.removeAt(0)
list.remove(4) // по значению
println(list) // [10, 2, 3]
list += 5 // оператор += работает как add
list -= 2
println(list) // [10, 3, 5]
list.sortDescending() // сортировка на месте
println(list) // [10, 5, 3]
}
```
## Set
```kotlin
fun main() {
val set = setOf(1, 2, 3)
println(2 in set) // true — O(1)
println(set.contains(5)) // false
println(setOf(1, 2, 2, 3).size) // 3 — дубликат отброшен
}
```
Операции над множествами:
```kotlin
fun main() {
val a = setOf(1, 2, 3)
val b = setOf(3, 4, 5)
println(a union b) // [1, 2, 3, 4, 5]
println(a intersect b) // [3]
println(a subtract b) // [1, 2]
println(a + b) // [1, 2, 3, 4, 5] — как union
println(a - b) // [1, 2]
}
```
Эти же функции работают на `List`, но результат всегда `Set`:
```kotlin
fun main() {
println(listOf(1, 1, 2) union listOf(2, 3)) // [1, 2, 3]
println(listOf(1, 1, 2).distinct()) // [1, 2] — а так останется List
}
```
### Три реализации Set
```kotlin
fun main() {
val linked = linkedSetOf(3, 1, 2) // порядок вставки
val hash = hashSetOf(3, 1, 2) // порядок не определён
val sorted = sortedSetOf(3, 1, 2) // отсортирован
println(linked) // [3, 1, 2]
println(sorted) // [1, 2, 3]
println(hash.size) // 3
}
```
| Реализация | Порядок | Поиск | Когда |
|---|---|---|---|
| `LinkedHashSet` (`setOf`) | вставки | O(1) | по умолчанию |
| `HashSet` (`hashSetOf`) | не определён | O(1) | экономия памяти, порядок не нужен |
| `TreeSet` (`sortedSetOf`) | сортировки | O(log n) | нужен отсортированный обход |
Элементы `Set` обязаны корректно реализовывать `equals` и `hashCode` - иначе множество работает неправильно. Подробнее: [equals, hashCode и toString](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/equals,%20hashCode%20и%20toString.md)
## Map
```kotlin
fun main() {
val map = mapOf("a" to 1, "b" to 2)
println(map["a"]) // 1
println(map["z"]) // null
println(map.getOrDefault("z", 0)) // 0
println(map.getOrElse("z") { -1 }) // -1
println(map.getValue("a")) // 1, бросает при отсутствии
println("a" in map) // true — проверка ключа
println(map.containsValue(2)) // true
println("${map.keys} ${map.values}") // [a, b] [1, 2]
}
```
`to` - инфиксная функция, создающая `Pair`:
```kotlin
fun main() {
val pair: Pair<String, Int> = "a" to 1
println("${pair.first} ${pair.second}") // a 1
val (k, v) = pair // деструктуризация
println("$k=$v") // a=1
println(listOf("a" to 1, "b" to 2).toMap()) // {a=1, b=2}
}
```
Изменение через `MutableMap`:
```kotlin
fun main() {
val map = mutableMapOf("a" to 1)
map["b"] = 2
map.put("c", 3)
map.putAll(mapOf("d" to 4))
map.remove("a")
println(map) // {b=2, c=3, d=4}
println(map.getOrPut("e") { 5 }) // 5 — вычислит и положит
println(map.getOrPut("e") { 99 }) // 5 — уже есть, лямбда не вызвана
map.merge("b", 10) { old, new -> old + new } // метод JDK
println(map) // {b=12, c=3, d=4, e=5}
}
```
`getOrPut` - основной инструмент кеширования: одна операция вместо проверки и вставки.
Обход:
```kotlin
fun main() {
val map = mapOf("a" to 1, "b" to 2)
for ((key, value) in map) print("$key=$value ") // a=1 b=2
println()
map.forEach { (key, value) -> print("$key$value ") } // a1 b2
println()
println(map.entries.first().key) // a
}
```
Деструктуризация в `for` и в лямбде - главная причина, почему обход `Map` в Kotlin короче, чем в Java.
Преобразования:
```kotlin
fun main() {
val map = mapOf("a" to 1, "b" to 2, "c" to 3)
println(map.filter { it.value > 1 }) // {b=2, c=3}
println(map.filterKeys { it != "a" }) // {b=2, c=3}
println(map.mapValues { it.value * 10 }) // {a=10, b=20, c=30}
println(map.mapKeys { it.key.uppercase() }) // {A=1, B=2, C=3}
println(map.map { "${it.key}${it.value}" }) // [a1, b2, c3] — это List
println(map.toList()) // [(a, 1), (b, 2), (c, 3)]
println(map.entries.sortedByDescending { it.value }.first().key) // c
}
```
### Три реализации Map
| Реализация | Порядок | Доступ | Когда |
|---|---|---|---|
| `LinkedHashMap` (`mapOf`) | вставки | O(1) | по умолчанию |
| `HashMap` (`hashMapOf`) | не определён | O(1) | экономия памяти |
| `TreeMap` (`sortedMapOf`) | сортировки ключей | O(log n) | диапазонные запросы |
## Выбор структуры
```mermaid
graph TD
A["Нужна коллекция"] --> B{"Пары ключ-значение?"}
B -->|да| M["Map"]
B -->|нет| C{"Дубликаты важны<br/>или нужен индекс?"}
C -->|да| L["List"]
C -->|нет| D{"Частый поиск<br/>элемента?"}
D -->|да| S["Set"]
D -->|нет| L
```
Практический критерий: если в коде есть `list.contains(x)` внутри цикла - это `Set`. Такая замена превращает O(n²) в O(n):
```kotlin
fun main() {
val ids = List(1000) { it }
val allowedList = List(1000) { it * 2 }
val allowedSet = allowedList.toSet()
println(ids.count { it in allowedList }) // O(n*m) — 1000*1000 сравнений
println(ids.count { it in allowedSet }) // O(n) — то же 500, но быстро
}
```
## Специальные коллекции
```kotlin
fun main() {
val deque = ArrayDeque(listOf(2, 3))
deque.addFirst(1)
deque.addLast(4)
println("$deque ${deque.removeFirst()} ${deque.removeLast()}") // [1, 2, 3, 4] 1 4
val grouped = listOf("яблоко", "арбуз", "банан").groupBy { it.first() }
println(grouped) // {я=[яблоко], а=[арбуз], б=[банан]}
val assoc = listOf("a", "bb", "ccc").associateWith { it.length }
println(assoc) // {a=1, bb=2, ccc=3}
}
```
`ArrayDeque` в Kotlin - своя реализация (не `java.util.ArrayDeque`), работает как стек и как очередь. Подробнее об `groupBy` и `associate`: [Операции](Операции.md)
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `map.get(k)` возвращает `V` | `map[k]` возвращает `V?` |
> | `map.put(k, v)` | `map[k] = v` |
> | `new AbstractMap.SimpleEntry<>(k, v)` | `k to v` |
> | `for (Map.Entry<K,V> e : map.entrySet())` | `for ((k, v) in map)` |
> | `map.computeIfAbsent(k, f)` | `map.getOrPut(k) { }` |
> | `Collections.unmodifiableList` | тип `List` |
> | `set.retainAll(other)` | `a intersect b` |
> | `list.get(i)` | `list[i]` |
>
> **`map[k]` возвращает nullable - и это правильно.** В Java `map.get(k)` имеет тип `V`, поэтому `int x = map.get("нет")` компилируется и даёт NPE при распаковке. Kotlin делает отсутствие ключа видимым в типе:
> ```kotlin
> fun main() {
> val map = mapOf("a" to 1)
> val x: Int = map["z"] ?: 0 // компилятор требует обработать null
> println(x) // 0
> }
> ```
> Обратная сторона: `map[k]!!` появляется в коде часто. Когда ключ гарантированно есть, честнее `getValue(k)` - он бросит `NoSuchElementException` с внятным сообщением.
>
> **Деструктуризация `Map.Entry`.** Java требует `e.getKey()`/`e.getValue()`; в Kotlin `for ((k, v) in map)` работает потому, что для `Map.Entry` объявлены функции-расширения `component1` и `component2`. Подробнее: [Деструктуризация](../Синтаксис%20и%20типы/Работа%20с%20данными/Деструктуризация.md)
>
> **Операции над множествами как инфиксные функции.** В Java `union` - это `addAll` на копии, `intersect` - `retainAll`, то есть операции мутирующие и требуют ручного копирования. В Kotlin `a union b` возвращает новое множество и читается как математическая запись.
>
> Сравнить: [HashMap в Java](../../Программирование%20на%20Java/Коллекции/HashMap.md)
> [!warning] Подводные камни
> **`map[k]` не различает «нет ключа» и «значение null»:**
> ```kotlin
> fun main() {
> val map = mapOf("a" to null, "b" to 1)
> println(map["a"]) // null
> println(map["z"]) // null — то же самое!
> println("a" in map) // true
> println("z" in map) // false — вот так различать
> }
> ```
>
> **`getValue` бросает, `get` - нет.** Это легко перепутать:
> ```kotlin
> fun main() {
> val map = mapOf("a" to 1)
> println(map["z"]) // null
> try {
> map.getValue("z")
> } catch (e: NoSuchElementException) {
> println("getValue бросает") // getValue бросает
> }
> }
> ```
>
> **Изменяемый элемент в `Set` или ключ в `Map` ломает коллекцию:**
> ```kotlin
> data class Key(var id: Int)
>
> fun main() {
> val key = Key(1)
> val set = mutableSetOf(key)
> println(key in set) // true
> key.id = 2 // hashCode изменился!
> println(key in set) // false — элемент потерян
> println(set.size) // 1 — но он всё ещё внутри
> }
> ```
> Ключи и элементы множеств обязаны быть неизменяемыми. `data class` с `var` в этой роли - ошибка.
>
> **`subList` - это вид на исходный список, а не копия:**
> ```kotlin
> fun main() {
> val list = mutableListOf(1, 2, 3, 4)
> val sub = list.subList(1, 3)
> list[1] = 99
> println(sub) // [99, 3] — изменился
> list.add(5)
> try {
> println(sub)
> } catch (e: ConcurrentModificationException) {
> println("структурное изменение списка сломало subList")
> }
> }
> ```
> Для независимой копии нужен `slice` или `toList()`.
>
> **`remove` на `MutableList<Int>` неоднозначен:**
> ```kotlin
> fun main() {
> val list = mutableListOf(10, 20, 30)
> list.removeAt(1) // по индексу
> println(list) // [10, 30]
> list.remove(10) // по значению
> println(list) // [30]
> }
> ```
> В Kotlin, в отличие от Java, перегрузки по индексу у `remove` нет - только `removeAt`. Это как раз устраняет классическую Java-ловушку `list.remove(1)`.
>
> **`list.contains(x)` в цикле - это O(n²).** Самая частая проблема производительности с коллекциями. Если проверка повторяется, предварительно постройте `Set`.
>
> **`hashSetOf` и `hashMapOf` не сохраняют порядок** - вывод может отличаться между запусками и версиями JDK. Тест, завязанный на порядок, будет нестабильным.
>
> **`sortedSetOf` требует `Comparable` или компаратор** и бросит `ClassCastException` при вставке несравнимого элемента - в рантайме, а не при компиляции.
>
> **`+=` на `List` работает, но создаёт новую коллекцию:**
> ```kotlin
> fun main() {
> var list = listOf(1, 2) // var!
> list += 3 // новый список, не add
> println(list) // [1, 2, 3]
>
> val mutable = mutableListOf(1, 2)
> mutable += 3 // здесь настоящий add
> println(mutable) // [1, 2, 3]
> }
> ```
> На `val list: List<Int>` оператор `+=` не скомпилируется. В цикле по большому списку `+=` на read-only коллекции даёт O(n²) копирований.
Далее: [Изменяемость](Изменяемость.md)
+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)
@@ -0,0 +1,323 @@
---
title: Изменяемость
description: Read-only против immutable, защитное копирование и персистентные коллекции
tags:
- kotlin
- collections
---
`List` в Kotlin - **read-only**, а не immutable. Это разные вещи, и их смешение - источник самых неприятных ошибок с коллекциями.
| | read-only (`List`) | immutable |
|---|---|---|
| изменить через эту ссылку | нельзя | нельзя |
| может измениться вообще | **да** | нет |
| гарантия | интерфейс без мутаторов | сам объект неизменяем |
| есть в Kotlin stdlib | да | нет (только внешняя библиотека) |
## Почему read-only не immutable
```kotlin
fun main() {
val mutable = mutableListOf(1, 2)
val readOnly: List<Int> = mutable // тот же объект, другой интерфейс
mutable.add(3)
println(readOnly) // [1, 2, 3] — изменился под вами
}
```
`MutableList` наследует `List`, поэтому любая изменяемая коллекция подходит там, где ожидается `List`. Приведение типа объект не копирует.
Это создаёт конкретную проблему в API:
```kotlin
class Repository4 {
private val items = mutableListOf(1, 2, 3)
fun getItems(): List<Int> = items // утечка: вернули ссылку на изменяемое
fun add(x: Int) { items.add(x) }
}
fun main() {
val repo = Repository4()
val snapshot = repo.getItems()
println(snapshot) // [1, 2, 3]
repo.add(4)
println(snapshot) // [1, 2, 3, 4] — «снимок» изменился
}
```
Клиент получил `List` и вправе считать, что значение стабильно - например, положить его в `Map` как ключ или обойти в другом потоке. Ни то ни другое здесь не безопасно.
## Защитное копирование
```kotlin
class Repository5 {
private val items = mutableListOf(1, 2, 3)
fun getItems(): List<Int> = items.toList() // копия
fun add(x: Int) { items.add(x) }
}
fun main() {
val repo = Repository5()
val snapshot = repo.getItems()
repo.add(4)
println("$snapshot / ${repo.getItems()}") // [1, 2, 3] / [1, 2, 3, 4]
}
```
`toList()` создаёт новый `ArrayList`. Цена - O(n) и лишний объект; выгода - значение, на которое можно полагаться.
Варианты копирования:
```kotlin
fun main() {
val source = mutableListOf(1, 2, 3)
println(source.toList()::class.simpleName) // ArrayList
println(source.toMutableList()::class.simpleName) // ArrayList
println(source.toSet()::class.simpleName) // LinkedHashSet
println(ArrayList(source)::class.simpleName) // ArrayList
println(List(source.size) { source[it] }) // [1, 2, 3]
}
```
Когда копировать:
- коллекция уходит за границу класса или модуля и должна быть стабильной
- коллекция попадает в `Map`/`Set` как ключ или элемент
- значение читается из другого потока
Когда не копировать:
- коллекция создана тут же и больше нигде не хранится
- вызывающий - код того же класса
- коллекция огромная, а изменения точно не будет (документируйте это)
## Неизменяемость на уровне содержимого
Даже честная копия списка не защищает от изменения **элементов**:
```kotlin
class Item(var name: String)
fun main() {
val items = listOf(Item("a"), Item("b"))
val copy = items.toList()
items[0].name = "изменено"
println(copy[0].name) // изменено — копия поверхностная
}
```
`toList()` копирует только ссылки. Настоящая защита - неизменяемые элементы:
```kotlin
data class ItemRo(val name: String)
fun main() {
val items = listOf(ItemRo("a"))
// items[0].name = "x" // ошибка: val
val changed = items.map { it.copy(name = "изменено") }
println("${items[0].name} / ${changed[0].name}") // a / изменено
}
```
Это общее правило: неизменяемость полезна, когда она рекурсивна. `List<T>` с изменяемым `T` даёт лишь половину гарантии.
## val и изменяемость
Два независимых понятия, которые постоянно путают:
```kotlin
fun main() {
val a = mutableListOf(1) // ссылку сменить нельзя, содержимое — можно
a.add(2)
println(a) // [1, 2]
// a = mutableListOf(3) // ошибка
var b = listOf(1) // ссылку сменить можно, содержимое — нельзя
b = listOf(1, 2)
println(b) // [1, 2]
// b.add(3) // ошибка
}
```
| | `val` | `var` |
|---|---|---|
| `List` | ничего не изменить | можно заменить целиком |
| `MutableList` | можно менять содержимое | и то и другое |
Самый частый правильный выбор - `val` + `List`. Подробнее: [Переменные](../Синтаксис%20и%20типы/Базовый%20синтаксис/Переменные.md)
## Функциональное обновление
Вместо изменения на месте - новое значение:
```kotlin
fun main() {
val original = listOf(1, 2, 3)
println(original + 4) // [1, 2, 3, 4]
println(original - 2) // [1, 3]
println(original.map { it * 10 }) // [10, 20, 30]
println(original.filter { it != 2 }) // [1, 3]
println(original.toMutableList().apply { this[0] = 99 }) // [99, 2, 3]
println(original) // [1, 2, 3] — не тронут
}
```
Для `Map` то же:
```kotlin
fun main() {
val config = mapOf("host" to "localhost", "port" to "8080")
println(config + ("port" to "9090")) // {host=localhost, port=9090}
println(config - "port") // {host=localhost}
println(config + mapOf("tls" to "on")) // {host=localhost, port=8080, tls=on}
}
```
Цена - копирование на каждую операцию. Для небольших коллекций и нечастых обновлений это правильный выбор по умолчанию: такой код проще отлаживать, потому что значение не меняется за спиной.
## Изменяемое состояние внутри, read-only снаружи
Рабочий шаблон для класса с коллекцией:
```kotlin
class Playlist {
private val _tracks = mutableListOf<String>()
val tracks: List<String> get() = _tracks.toList() // копия при чтении
fun add(track: String) { _tracks.add(track) }
fun size() = _tracks.size
}
fun main() {
val p = Playlist()
p.add("песня 1")
val snapshot = p.tracks
p.add("песня 2")
println("$snapshot / ${p.tracks}") // [песня 1] / [песня 1, песня 2]
}
```
Если копия на каждое чтение слишком дорога, альтернатива - возвращать `_tracks` напрямую и документировать, что значение живое. Тогда тип должен быть осмысленным: `List` здесь обманывает читателя, и честнее отдавать `Sequence` или явный метод `snapshot()`.
## Неизменяемость и многопоточность
```kotlin
fun main() {
val shared = mutableListOf(1, 2, 3)
// Небезопасно: обход в одном потоке + add в другом = ConcurrentModificationException
// Безопасно: работать с копией
val safe = shared.toList()
println(safe.sum()) // 6
val sync = java.util.Collections.synchronizedList(mutableListOf(1, 2))
println(sync.size) // 2 — но обход всё равно нужно синхронизировать вручную
val concurrent = java.util.concurrent.CopyOnWriteArrayList(listOf(1, 2))
println(concurrent.size) // 2
}
```
Неизменяемое значение потокобезопасно по определению - именно поэтому в конкурентном коде предпочитают read-only копии, а не блокировки. Подробнее: [Разделяемое состояние](../Корутины/Разделяемое%20состояние.md)
## Персистентные коллекции
В stdlib настоящих immutable-коллекций нет. Их даёт библиотека `kotlinx.collections.immutable`:
```kotlin
// Требует зависимости org.jetbrains.kotlinx:kotlinx-collections-immutable.
// val list: PersistentList<Int> = persistentListOf(1, 2, 3)
// val updated = list.add(4) // новый список, старый цел
// println("$list / $updated") // [1, 2, 3] / [1, 2, 3, 4]
//
// Тип ImmutableList<T> гарантирует, что коллекция не изменится вообще,
// а PersistentList<T> добавляет эффективное «обновление» без полного копирования.
```
Отличие от `toList()`: персистентная коллекция при добавлении переиспользует внутреннюю структуру, поэтому `add` стоит O(log n), а не O(n). Это имеет смысл, когда обновления частые, а коллекция большая.
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `Collections.unmodifiableList(x)` | тип `List<T>` |
> | `List.copyOf(x)` (Java 10+) | `x.toList()` |
> | `List.of(1, 2)` - immutable | `listOf(1, 2)` - read-only |
> | нарушение видно в рантайме | видно при компиляции |
> | `final List` | `val list` |
> | Guava `ImmutableList` | `kotlinx.collections.immutable` |
>
> **`unmodifiableList` и `List` решают одну задачу по-разному.** Java-обёртка - объект-декоратор: тип остаётся `List`, метод `add` есть, но бросает исключение в рантайме. Kotlin убирает метод из интерфейса, поэтому ошибка видна компилятору и обёртка не нужна:
> ```java
> List<Integer> ro = Collections.unmodifiableList(src);
> ro.add(1); // компилируется, падает в рантайме
> ```
> Но у Java-обёртки есть преимущество, которого нет у Kotlin: она действительно запрещает изменение через **эту** ссылку даже при касте. Kotlin-ский `List` каст к `MutableList` может пережить, если под ним `ArrayList`.
>
> **`List.of` в Java сильнее, чем `listOf` в Kotlin.** `List.of(1, 2)` создаёт по-настоящему неизменяемую коллекцию: любая попытка изменения, в том числе из другого места, бросит исключение. `listOf(1, 2)` даёт неизменяемую реализацию только случайно - это деталь, на которую нельзя полагаться, потому что `listOf` на самом деле описывает интерфейс, а не гарантию объекта.
>
> **Защитное копирование нужно в обоих языках.** `private final List` в Java и `private val` в Kotlin одинаково не защищают содержимое. Разница в том, что в Kotlin ошибку легче не заметить: тип `List` в сигнатуре выглядит как обещание неизменяемости, которого он не даёт.
>
> Сравнить: [Инкапсуляция в Java](../../Программирование%20на%20Java/ООП/Инкапсуляция/Инкапсуляция.md)
> [!warning] Подводные камни
> **Возврат `MutableList` под типом `List` - утечка:**
> ```kotlin
> class Leaky2 {
> private val items = mutableListOf(1, 2)
> fun items(): List<Int> = items
> }
>
> fun main() {
> val l = Leaky2()
> val view = l.items()
> @Suppress("UNCHECKED_CAST")
> (view as MutableList<Int>).add(3) // клиент изменил приватное состояние
> println(l.items()) // [1, 2, 3]
> }
> ```
> Каст сработал, потому что под ссылкой настоящий `ArrayList`. Защита - `toList()`.
>
> **`toList()` копирует поверхностно** - элементы остаются общими. Для вложенных структур нужна глубокая копия:
> ```kotlin
> fun main() {
> val nested = listOf(mutableListOf(1), mutableListOf(2))
> val shallow = nested.toList()
> nested[0].add(99)
> println(shallow[0]) // [1, 99] — внутренний список общий
>
> val deep = nested.map { it.toList() }
> nested[0].add(100)
> println(deep[0]) // [1, 99] — а этот цел
> }
> ```
>
> **`val` не делает коллекцию неизменяемой** - только ссылку. `val items = mutableListOf<Int>()` изменяется свободно.
>
> **Из Java read-only коллекция изменяема:**
> ```kotlin
> fun provide(): List<Int> = mutableListOf(1, 2)
>
> fun main() { println(provide()) } // [1, 2]
> ```
> Java-код получит `java.util.List` и вызовет `add` без ошибок компиляции. Для API, доступного из Java, копируйте или используйте `Collections.unmodifiableList`. Подробнее: [Kotlin из Java](../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md)
>
> **Изменяемый объект как ключ `Map` теряется:**
> ```kotlin
> data class MutableKey(var id: Int)
>
> fun main() {
> val key = MutableKey(1)
> val map = mutableMapOf(key to "значение")
> key.id = 2
> println(map[key]) // null — хеш изменился
> println(map.size) // 1 — запись на месте, но недостижима
> }
> ```
>
> **`+=` на `var list: List<T>` в цикле - это O(n²):**
> ```kotlin
> fun main() {
> var slow = listOf<Int>()
> repeat(1000) { slow += it } // 1000 копирований списка
> println(slow.size) // 1000
>
> val fast = buildList { repeat(1000) { add(it) } }
> println(fast.size) // 1000, одна коллекция
> }
> ```
>
> **`ConcurrentModificationException` при изменении во время обхода:**
> ```kotlin
> fun main() {
> val list = mutableListOf(1, 2, 3)
> try {
> for (x in list) if (x == 2) list.remove(x)
> } catch (e: ConcurrentModificationException) {
> println("нельзя изменять во время обхода")
> }
> list.removeAll { it == 2 } // так правильно
> println(list) // [1, 3]
> }
> ```
>
> **Обёртка `synchronizedList` не делает обход потокобезопасным** - атомарны только отдельные методы. Итерацию нужно оборачивать в `synchronized` вручную.
Далее: [Операции](Операции.md)
@@ -0,0 +1,282 @@
---
title: Коллекции под капотом
description: Что лежит в байт-коде, сложность операций, расход памяти и упаковка примитивов
tags:
- kotlin
- collections
- performance
---
Отдельных классов коллекций у Kotlin нет. `List`, `MutableList`, `Map` - это интерфейсы, которые при компиляции отображаются в типы `java.util`.
```kotlin
fun main() {
println(listOf(1, 2, 3).javaClass.name) // java.util.Arrays$ArrayList
println(mutableListOf(1, 2).javaClass.name) // java.util.ArrayList
println(setOf(1, 2).javaClass.name) // java.util.LinkedHashSet
println(mapOf("a" to 1, "b" to 2).javaClass.name) // java.util.LinkedHashMap
println(emptyList<Int>().javaClass.name) // kotlin.collections.EmptyList
println(listOf(1).javaClass.name) // java.util.Collections$SingletonList
println(mapOf("a" to 1).javaClass.name) // java.util.Collections$SingletonMap
}
```
Обратите внимание: `listOf` возвращает **три разные** реализации в зависимости от числа аргументов, и `mapOf` ведёт себя так же. Это оптимизация под частые случаи - пустая коллекция и коллекция из одного элемента не требуют ни массива, ни хеш-таблицы.
## Соответствие типов
| Kotlin | JVM | Примечание |
|---|---|---|
| `List<T>` | `java.util.List` | тот же интерфейс |
| `MutableList<T>` | `java.util.List` | **тот же**, различия только в компиляторе |
| `Map<K, V>` | `java.util.Map` | |
| `Array<T>` | `T[]` | настоящий массив |
| `IntArray` | `int[]` | без упаковки |
| `Array<Int>` | `Integer[]` | с упаковкой |
`List` и `MutableList` - это один и тот же `java.util.List` в байт-коде. Разделение существует только при компиляции Kotlin, что объясняет две вещи: из Java read-only коллекция изменяема, и каст `List` в `MutableList` компилируется.
```kotlin
fun main() {
val ro: List<Int> = mutableListOf(1, 2)
println(ro is MutableList<*>) // true — в рантайме это один тип
}
```
## Сложность операций
| Операция | `ArrayList` | `LinkedHashSet` | `LinkedHashMap` | `TreeMap` |
|---|---|---|---|---|
| доступ по индексу | O(1) | - | - | - |
| поиск элемента | O(n) | O(1) | O(1) по ключу | O(log n) |
| вставка в конец | O(1)* | O(1) | O(1) | O(log n) |
| вставка в начало | O(n) | O(1) | O(1) | O(log n) |
| удаление по индексу | O(n) | - | - | - |
| удаление по значению | O(n) | O(1) | O(1) | O(log n) |
\* амортизированная: при переполнении массив копируется в новый, вдвое больший.
Практические следствия:
```kotlin
fun main() {
val list = List(10_000) { it }
val set = list.toSet()
// O(n) против O(1)
println(9999 in list) // true, но обошёл весь список
println(9999 in set) // true, один хеш
// вставка в начало ArrayList — сдвиг всего массива
val deque = ArrayDeque(list)
deque.addFirst(-1) // O(1)
println(deque.first()) // -1
}
```
## Упаковка примитивов
Главный источник неожиданных расходов памяти:
```kotlin
fun main() {
val boxed: List<Int> = List(5) { it } // каждый элемент — объект Integer
val primitive: IntArray = IntArray(5) { it } // настоящий int[]
println(boxed.sum()) // 10
println(primitive.sum()) // 10
println(primitive.javaClass.name) // [I — массив int
}
```
Разница в памяти на 1 млн элементов:
| Структура | Примерный размер |
|---|---|
| `IntArray(1_000_000)` | ~4 МБ |
| `Array<Int>(1_000_000)` | ~20 МБ (ссылки + объекты) |
| `List<Int>` из 1 млн | ~20 МБ |
Кеш `Integer` смягчает это для малых чисел:
```kotlin
fun main() {
val a: Int? = 127
val b: Int? = 127
println(a === b) // true — из кеша -128..127
val c: Int? = 128
val d: Int? = 128
println(c === d) // false — разные объекты
println(c == d) // true — значения равны
}
```
Это ровно та же ловушка, что с `Integer` в Java. Правило: для чисел всегда `==`, никогда `===`. Подробнее: [Числа и переполнение](../Синтаксис%20и%20типы/Работа%20с%20данными/Числа%20и%20переполнение.md)
Специализированные массивы без упаковки:
```kotlin
fun main() {
println(intArrayOf(1, 2, 3).sum()) // 6
println(doubleArrayOf(1.0, 2.0).average()) // 1.5
println(booleanArrayOf(true, false).count { it }) // 1
println(longArrayOf(1L).size) // 1
}
```
Их стоит использовать, когда элементов много и они числовые: матрицы, буферы, обработка сигналов. В обычном бизнес-коде разница несущественна.
## Рост ArrayList
```kotlin
fun main() {
val withoutCapacity = ArrayList<Int>() // начальная ёмкость 10
repeat(1000) { withoutCapacity.add(it) } // ~8 перевыделений массива
val withCapacity = ArrayList<Int>(1000) // сразу нужный размер
repeat(1000) { withCapacity.add(it) } // перевыделений нет
println("${withoutCapacity.size} ${withCapacity.size}") // 1000 1000
}
```
При каждом переполнении создаётся массив в 1.5 раза больше и копируется содержимое. Для заранее известного размера задавайте ёмкость - это убирает лог n копирований.
## Что делает цепочка операций
```kotlin
fun main() {
val source = List(1000) { it }
val result = source
.map { it * 2 } // новый ArrayList на 1000
.filter { it > 100 } // новый ArrayList на ~950
.take(5) // новый ArrayList на 5
println(result) // [102, 104, 106, 108, 110]
}
```
Три промежуточных списка, 1000 + 950 операций ради 5 результатов. Та же цепочка на `Sequence` не создаст ни одного промежуточного списка и выполнит ~56 операций. Подробнее: [Sequence](Sequence.md)
Измерить разницу можно так:
```kotlin
fun main() {
val source = List(1_000_000) { it }
val t1 = System.nanoTime()
val a = source.map { it * 2 }.filter { it > 100 }.first()
val eager = (System.nanoTime() - t1) / 1_000_000
val t2 = System.nanoTime()
val b = source.asSequence().map { it * 2 }.filter { it > 100 }.first()
val lazy = (System.nanoTime() - t2) / 1_000_000
println("$a == $b") // 102 == 102
println("list ${eager}мс, sequence ${lazy}мс — ленивый быстрее: ${lazy < eager}")
}
```
## Inline и отсутствие объектов-лямбд
Операции над коллекциями объявлены `inline`, поэтому лямбда не создаёт объект:
```kotlin
fun main() {
val sum = listOf(1, 2, 3).filter { it > 1 }.sumOf { it * 2 }
println(sum) // 10
}
```
Компилятор подставляет тело лямбды прямо в цикл - в байт-коде нет ни объекта `Function1`, ни виртуального вызова. Это то, чего не даёт Java-стрим: там каждая лямбда - объект, а цепочка строит конвейер из `Spliterator`-ов.
Исключение - `Sequence`: там операции не могут быть полностью inline, потому что элементы обрабатываются лениво через итератор. Это и есть источник накладных расходов `Sequence` на малых данных. Подробнее: [Inline-функции](../Функциональный%20Kotlin/Inline-функции.md)
## Как выбирают структуру
```kotlin
fun main() {
// частый поиск -> Set
val allowed = setOf("a", "b", "c")
println(listOf("a", "x").count { it in allowed }) // 1
// частое добавление в начало и конец -> ArrayDeque
val queue = ArrayDeque<Int>()
queue.addLast(1); queue.addFirst(0)
println(queue) // [0, 1]
// нужен отсортированный обход и диапазоны -> TreeMap
val sorted = sortedMapOf(3 to "c", 1 to "a", 2 to "b")
println(sorted.headMap(3)) // {1=a, 2=b}
// много числовых данных -> примитивный массив
val buffer = IntArray(1000)
println(buffer.size) // 1000
}
```
## Потокобезопасность
```kotlin
import java.util.concurrent.ConcurrentHashMap
fun main() {
val map = ConcurrentHashMap<String, Int>()
map["a"] = 1
map.compute("a") { _, v -> (v ?: 0) + 1 }
println(map) // {a=2}
val sync = java.util.Collections.synchronizedMap(mutableMapOf("a" to 1))
println(sync.size) // 1
}
```
Обычные коллекции Kotlin не потокобезопасны - это те же `ArrayList` и `HashMap`. Варианты:
- read-only копия на каждый поток (`toList()`) - проще всего
- `ConcurrentHashMap` и `CopyOnWriteArrayList` из JDK
- корутины с ограничением доступа одним потоком или `Mutex`
Подробнее: [Разделяемое состояние](../Корутины/Разделяемое%20состояние.md)
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `ArrayList` явно | `mutableListOf()` -> `ArrayList` |
> | `new HashMap<>()` | `mutableMapOf()` -> `LinkedHashMap` |
> | `IntStream` без упаковки | `IntArray` или Java-стрим |
> | лямбда - объект `Function` | лямбда инлайнится |
> | `Stream` ленив | `Iterable`-операции энергичны |
> | `new ArrayList<>(100)` | `ArrayList(100)` |
>
> **Коллекции те же самые - это не «Kotlin-коллекции».** Любая `List<T>` из Kotlin принимается Java-методом, принимающим `java.util.List`, без конвертации и накладных расходов. Это принципиальное отличие от языков со своей библиотекой коллекций (например, Scala), где на границе нужны обёртки.
>
> **Умолчание `LinkedHashMap` вместо `HashMap`.** `mutableMapOf()` создаёт `LinkedHashMap`, который на ~20% тяжелее по памяти (хранит ссылки на соседей), но даёт предсказуемый порядок обхода. Если порядок не нужен и записей миллионы, `hashMapOf()` экономнее.
>
> **Инлайн лямбд - преимущество перед стримами.** В Java `list.stream().filter(x -> x > 1)` создаёт объект лямбды и конвейер стрима. В Kotlin `list.filter { it > 1 }` компилируется в обычный цикл `for` с телом лямбды внутри - объектов не создаётся вовсе. Для коротких цепочек на небольших коллекциях Kotlin обычно быстрее Java-стримов именно поэтому.
>
> **Нет `IntStream`.** Java даёт специализации для `int`, `long`, `double`, избегающие упаковки в цепочках. В Kotlin эквивалента нет: `Sequence<Int>` упаковывает. Для числовых расчётов на больших объёмах используйте `IntArray` с обычным циклом или вызывайте Java-стримы.
>
> Сравнить: [HashMap в Java](../../Программирование%20на%20Java/Коллекции/HashMap.md), [JIT](../../Программирование%20на%20Java/JVM/JIT.md)
> [!warning] Подводные камни
> **`listOf` возвращает разные классы** - полагаться на конкретный тип нельзя:
> ```kotlin
> fun main() {
> println(listOf<Int>().javaClass.simpleName) // EmptyList
> println(listOf(1).javaClass.simpleName) // SingletonList
> println(listOf(1, 2).javaClass.simpleName) // ArrayList (Arrays$ArrayList)
> }
> ```
> Ни один из них не поддерживает `add`, но по разным причинам, и сообщения об ошибках будут разными.
>
> **`Arrays$ArrayList` - не `java.util.ArrayList`.** Это обёртка над массивом с фиксированным размером: `set` работает, `add` бросает `UnsupportedOperationException`.
>
> **`List<Int>` упаковывает каждое число.** Для миллиона элементов это ~20 МБ вместо 4 МБ и нагрузка на сборщик мусора. В горячем численном коде используйте `IntArray`.
>
> **`===` на числах даёт разные результаты в зависимости от значения** - из-за кеша `Integer` в диапазоне -128..127. Сравнивайте только через `==`.
>
> **`list.contains` в цикле - O(n²).** Самая частая проблема производительности:
> ```kotlin
> fun main() {
> val a = List(2000) { it }
> val b = List(2000) { it * 2 }
> println(a.count { it in b }) // 4 млн сравнений
> val bSet = b.toSet()
> println(a.count { it in bSet }) // 2000 операций, тот же результат
> }
> ```
>
> **Каждая операция создаёт коллекцию.** Цепочка из пяти операций по большому списку - пять списков в памяти одновременно. Для больших данных нужен `Sequence`.
>
> **`ArrayList` без начальной ёмкости перевыделяет массив.** При известном размере задавайте его: `ArrayList(n)` или `buildList(n) { }`.
>
> **`subList` держит ссылку на исходный список** - он не будет собран сборщиком мусора, даже если нужен только маленький срез большого списка. Для среза, который переживёт оригинал, используйте `slice` или `toList`.
>
> **Коллекции Kotlin не потокобезопасны.** `mutableListOf()` - обычный `ArrayList`. Одновременное изменение из двух потоков повреждает структуру или бросает `ConcurrentModificationException`.
>
> **Изменяемый ключ ломает `HashMap`.** Запись становится недостижимой, хотя `size` её считает. Ключи должны быть неизменяемыми - `data class` с `val`-полями. Подробнее: [equals, hashCode и toString](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/equals,%20hashCode%20и%20toString.md)
>
> **Измерения производительности на JVM без прогрева врут.** Пример с `System.nanoTime` выше показывает порядок величины, но JIT компилирует горячий код только после тысяч вызовов. Для настоящих измерений нужен JMH.
Далее: [Параметрический полиморфизм](../Дженерики/Параметрический%20полиморфизм.md)
@@ -0,0 +1,234 @@
---
title: Коллекции
description: Обзор иерархии коллекций Kotlin, разделение на читаемые и изменяемые интерфейсы
tags:
- kotlin
- collections
---
Коллекции Kotlin - это **те же** классы JDK (`ArrayList`, `HashMap`, `LinkedHashSet`), но с другими интерфейсами поверх них. Главное отличие от Java: изменяемость выражена в системе типов.
```kotlin
fun main() {
val readOnly: List<Int> = listOf(1, 2, 3)
val mutable: MutableList<Int> = mutableListOf(1, 2, 3)
mutable.add(4)
// readOnly.add(4) // ошибка: у List нет метода add
println("$readOnly / $mutable") // [1, 2, 3] / [1, 2, 3, 4]
println(readOnly.javaClass.name) // java.util.Arrays$ArrayList
println(mutable.javaClass.name) // java.util.ArrayList
}
```
## Иерархия
```mermaid
graph TD
I["Iterable&lt;out T&gt;"] --> C["Collection&lt;out T&gt;"]
C --> L["List&lt;out T&gt;"]
C --> S["Set&lt;out T&gt;"]
I --> MI["MutableIterable&lt;T&gt;"]
MI --> MC["MutableCollection&lt;T&gt;"]
C --> MC
MC --> ML["MutableList&lt;T&gt;"]
L --> ML
MC --> MS["MutableSet&lt;T&gt;"]
S --> MS
M["Map&lt;K, out V&gt;"] --> MM["MutableMap&lt;K, V&gt;"]
```
Два важных факта из этой схемы:
- `MutableList` **наследует** `List`, то есть изменяемую коллекцию можно передать туда, где ожидается читаемая. Обратное невозможно.
- `Map` не входит в `Collection` - это отдельная иерархия, как и в Java.
| Интерфейс | Чтение | Изменение | Реализация по умолчанию |
|---|---|---|---|
| `List<T>` | да | нет | `ArrayList` под капотом |
| `MutableList<T>` | да | да | `ArrayList` |
| `Set<T>` | да | нет | `LinkedHashSet` |
| `MutableSet<T>` | да | да | `LinkedHashSet` |
| `Map<K, V>` | да | нет | `LinkedHashMap` |
| `MutableMap<K, V>` | да | да | `LinkedHashMap` |
Обратите внимание: умолчание для `Set` и `Map` - **`Linked`**-версии, то есть порядок вставки сохраняется. В Java `new HashSet<>()` порядок не гарантирует.
## Создание
```kotlin
fun main() {
val a = listOf(1, 2, 3) // read-only
val b = mutableListOf(1, 2, 3) // изменяемый
val c = emptyList<Int>() // пустой
val d = listOfNotNull(1, null, 3) // null отфильтрованы
val e = List(3) { it * it } // по индексу
println("$a $b $c $d $e") // [1, 2, 3] [1, 2, 3] [] [1, 3] [0, 1, 4]
val s = setOf(1, 2, 2, 3) // дубликаты убраны
val m = mapOf("a" to 1, "b" to 2)
println("$s $m") // [1, 2, 3] {a=1, b=2}
}
```
Специализированные варианты для разных реализаций:
```kotlin
fun main() {
val sorted = sortedSetOf(3, 1, 2) // TreeSet
val sortedMap = sortedMapOf("b" to 2, "a" to 1)
val linked = linkedSetOf(3, 1, 2) // LinkedHashSet
val hash = hashSetOf(3, 1, 2) // HashSet, порядок не гарантирован
val arrayDeque = ArrayDeque(listOf(1, 2, 3))
println("$sorted $sortedMap $linked") // [1, 2, 3] {a=1, b=2} [3, 1, 2]
println(hash.size) // 3
arrayDeque.addFirst(0)
println(arrayDeque) // [0, 1, 2, 3]
}
```
## Билдеры
Когда коллекцию нужно собрать по условию, а результат отдать как read-only:
```kotlin
fun main() {
val list = buildList {
add(1)
if (true) add(2)
addAll(listOf(3, 4))
}
println(list) // [1, 2, 3, 4]
val map = buildMap {
put("a", 1)
putAll(mapOf("b" to 2))
}
println(map) // {a=1, b=2}
val set = buildSet { repeat(3) { add(it % 2) } }
println(set) // [0, 1]
}
```
Внутри лямбды коллекция изменяемая, снаружи возвращается `List<T>`. Это чище, чем `mutableListOf` с последующим приведением.
## Пустая коллекция и null
```kotlin
fun main() {
val nullable: List<Int>? = null
println(nullable?.size ?: 0) // 0
println(nullable.orEmpty()) // []
println(nullable.isNullOrEmpty()) // true
val empty = emptyList<Int>()
println(empty.firstOrNull()) // null
println(empty.sum()) // 0
}
```
Практическое правило: в API возвращайте пустую коллекцию, а не `null`. `List<T>?` заставляет каждого клиента писать `?.` или `orEmpty()`, не давая взамен никакой информации - пустой список уже означает «ничего нет».
## Массив не коллекция
```kotlin
fun main() {
val array = arrayOf(1, 2, 3)
// array.filter — есть, потому что Array получает расширения из Iterable
println(array.filter { it > 1 }) // [2, 3] — это уже List
println(array.toList()::class.simpleName) // ArrayList
println(listOf(1, 2).toTypedArray().size) // 2
}
```
`Array<T>` - отдельный тип, не входящий в `Collection`. У него фиксированный размер и инвариантность. Подробнее: [Массивы](../Синтаксис%20и%20типы/Работа%20с%20данными/Массивы.md)
## Карта раздела
| Заметка | О чём |
|---|---|
| [List, Set, Map](List,%20Set,%20Map.md) | три семейства, их гарантии и выбор между ними |
| [Изменяемость](Изменяемость.md) | read-only против immutable, защита от изменения |
| [Операции](Операции.md) | `map`, `filter`, `groupBy`, `fold` и остальной арсенал |
| [Sequence](Sequence.md) | ленивые вычисления и когда они выигрывают |
| [Коллекции под капотом](Коллекции%20под%20капотом.md) | что лежит в байт-коде, сложность операций, память |
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `new ArrayList<>()` | `mutableListOf()` |
> | `List.of(1, 2)` (Java 9+) | `listOf(1, 2)` |
> | `Collections.emptyList()` | `emptyList()` |
> | изменяемость не в типе | `List` против `MutableList` |
> | `UnsupportedOperationException` в рантайме | ошибка компиляции |
> | `Stream` для операций | расширения прямо на коллекции |
> | `new HashSet<>()` - порядок не гарантирован | `setOf()` - порядок вставки |
>
> **Разделение на `List` и `MutableList` - главное нововведение.** В Java `List` - один интерфейс с методом `add`, и `List.of(1, 2)` его тоже имеет: вызов просто бросает `UnsupportedOperationException` в рантайме.
> ```java
> List<Integer> list = List.of(1, 2);
> list.add(3); // компилируется, падает в рантайме
> ```
> ```kotlin
> fun main() {
> val list = listOf(1, 2)
> // list.add(3) // не компилируется
> println(list.size)
> }
> ```
> Ошибка переехала из рантайма в компиляцию - это главная практическая польза.
>
> **Операции не требуют `Stream`.** В Java цепочка выглядит как `list.stream().filter(...).map(...).collect(Collectors.toList())`. В Kotlin `filter` и `map` - функции-расширения прямо на `Iterable`, они сразу возвращают `List`:
> ```kotlin
> fun main() {
> println(listOf(1, 2, 3).filter { it > 1 }.map { it * 10 }) // [20, 30]
> }
> ```
> Цена - промежуточный список на каждом шаге. Ленивый аналог `Stream` - это [Sequence](Sequence.md).
>
> **`setOf` и `mapOf` сохраняют порядок вставки**, потому что под ними `LinkedHashSet` и `LinkedHashMap`. Это удобно (вывод предсказуем), но на порядок всё равно не стоит полагаться в контракте: тип объявлен как `Set`, а не как «упорядоченное множество».
>
> Сравнить: [HashMap в Java](../../Программирование%20на%20Java/Коллекции/HashMap.md)
> [!warning] Подводные камни
> **`List` - это read-only, а не immutable.** Объект под ним может меняться через другую ссылку:
> ```kotlin
> fun main() {
> val mutable = mutableListOf(1, 2)
> val readOnly: List<Int> = mutable // та же коллекция!
> mutable.add(3)
> println(readOnly) // [1, 2, 3] — изменился
> }
> ```
> `List` гарантирует только то, что **вы** через эту ссылку не измените коллекцию. Подробнее: [Изменяемость](Изменяемость.md)
>
> **Из Java любой `List` изменяем.** Типы Kotlin-коллекций - это `java.util.List`, поэтому Java-код про read-only не знает:
> ```kotlin
> fun giveList(): List<Int> = mutableListOf(1, 2)
>
> fun main() { println(giveList().size) } // 2
> ```
> Java-клиент вызовет `add` без всяких ошибок компиляции, и если под ссылкой `ArrayList` - элемент добавится. Для настоящей защиты нужна копия.
>
> **`listOf()` возвращает разные реализации.** `listOf()` без аргументов - `EmptyList`, `listOf(x)` - `SingletonList`, с несколькими - `Arrays$ArrayList`. То же у `mapOf` и `setOf`: на одном элементе это `SingletonMap`/`SingletonSet`, а не `Linked`-версия. Последний не поддерживает `add` даже через каст:
> ```kotlin
> fun main() {
> val list = listOf(1, 2)
> try {
> @Suppress("UNCHECKED_CAST")
> (list as MutableList<Int>).add(3)
> } catch (e: UnsupportedOperationException) {
> println("каст скомпилировался, но add упал")
> }
> }
> ```
> Приводить `List` к `MutableList` нельзя: иногда это сработает, иногда бросит исключение.
>
> **`Map` не `Collection`** - у него нет `size` через `Collection`-интерфейс, `filter` работает с `Map.Entry`, а `map` возвращает `List`:
> ```kotlin
> fun main() {
> val m = mapOf("a" to 1, "b" to 2)
> println(m.filter { it.value > 1 }) // {b=2} — остался Map
> println(m.map { it.key }) // [a, b] — стал List
> println(m.mapValues { it.value * 10 }) // {a=10, b=20}
> }
> ```
>
> **Операции создают новую коллекцию на каждом шаге.** Цепочка из пяти `map`/`filter` по списку в миллион элементов создаст пять списков. Для длинных цепочек и больших данных нужен [Sequence](Sequence.md).
>
> **`hashSetOf` и `hashMapOf` не гарантируют порядок.** Если тест полагается на порядок обхода, он может сломаться при смене JDK. Используйте `setOf`/`mapOf` или сортируйте явно.
Далее: [List, Set, Map](List,%20Set,%20Map.md)
@@ -0,0 +1,424 @@
---
title: Операции
description: map, filter, groupBy, fold, zip и остальной арсенал операций над коллекциями
tags:
- kotlin
- collections
---
Операции над коллекциями - функции-расширения на `Iterable<T>`. Их около 150; здесь - те, что покрывают почти всю практику.
Все они делятся на две группы:
- **возвращают коллекцию**: `map`, `filter`, `sorted` - можно строить цепочки
- **возвращают одно значение**: `sum`, `first`, `count`, `fold` - завершают цепочку
## Преобразование
```kotlin
fun main() {
val list = listOf(1, 2, 3)
println(list.map { it * 2 }) // [2, 4, 6]
println(list.mapIndexed { i, x -> "$i:$x" }) // [0:1, 1:2, 2:3]
println(listOf("1", "a", "3").mapNotNull { it.toIntOrNull() }) // [1, 3]
println(list.flatMap { listOf(it, it * 10) }) // [1, 10, 2, 20, 3, 30]
println(listOf(listOf(1, 2), listOf(3)).flatten()) // [1, 2, 3]
println(list.zip(listOf("a", "b"))) // [(1, a), (2, b)]
println(list.withIndex().map { "${it.index}${it.value}" }) // [01, 12, 23]
}
```
`mapNotNull` - самая полезная из них: преобразование с отбрасыванием неудач в одном шаге, вместо `map` + `filterNotNull`.
`flatMap` нужен, когда преобразование даёт коллекцию, а результат нужен плоским:
```kotlin
data class Order5(val id: Int, val items: List<String>)
fun main() {
val orders = listOf(
Order5(1, listOf("хлеб", "молоко")),
Order5(2, listOf("сыр")),
)
println(orders.flatMap { it.items }) // [хлеб, молоко, сыр]
println(orders.map { it.items }) // [[хлеб, молоко], [сыр]]
}
```
## Фильтрация
```kotlin
fun main() {
val list = listOf(1, 2, 3, 4, 5)
println(list.filter { it % 2 == 0 }) // [2, 4]
println(list.filterNot { it % 2 == 0 }) // [1, 3, 5]
println(list.filterIndexed { i, _ -> i > 2 }) // [4, 5]
println(listOf(1, null, 3).filterNotNull()) // [1, 3]
println(listOf<Any>(1, "a", 2).filterIsInstance<Int>()) // [1, 2]
println(list.take(2)) // [1, 2]
println(list.takeWhile { it < 3 }) // [1, 2]
println(list.dropWhile { it < 3 }) // [3, 4, 5]
println(list.distinct()) // [1, 2, 3, 4, 5]
println(listOf("aa", "ab", "ba").distinctBy { it.first() }) // [aa, ba]
}
```
`partition` делит на две коллекции за один проход:
```kotlin
fun main() {
val (even, odd) = listOf(1, 2, 3, 4).partition { it % 2 == 0 }
println("$even $odd") // [2, 4] [1, 3]
}
```
## Поиск и проверки
```kotlin
fun main() {
val list = listOf(1, 2, 3, 4)
println(list.first { it > 2 }) // 3, бросает если нет
println(list.firstOrNull { it > 10 }) // null
println(list.find { it > 2 }) // 3 — то же, что firstOrNull
println(list.last()) // 4
println(list.single { it == 2 }) // 2, бросает если не ровно один
println(list.singleOrNull { it > 2 }) // null — их два
println(list.any { it > 3 }) // true
println(list.all { it > 0 }) // true
println(list.none { it > 10 }) // true
println(list.count { it % 2 == 0 }) // 2
println(3 in list) // true
println(list.indexOfFirst { it > 2 }) // 2
}
```
Поведение на пустой коллекции - источник ошибок:
```kotlin
fun main() {
val empty = emptyList<Int>()
println(empty.any { it > 0 }) // false
println(empty.all { it > 0 }) // true — вакуумная истина!
println(empty.none { it > 0 }) // true
println(empty.sum()) // 0
println(empty.firstOrNull()) // null
println(empty.maxOrNull()) // null
}
```
`all` на пустой коллекции возвращает `true` - это математически корректно, но в проверках вида «все элементы валидны» часто неожиданно.
## Агрегация
```kotlin
fun main() {
val list = listOf(1, 2, 3, 4)
println(list.sum()) // 10
println(list.average()) // 2.5
println(list.maxOrNull()) // 4
println(list.minOrNull()) // 1
println(list.sumOf { it * 10 }) // 100
println(list.maxByOrNull { -it }) // 1
println(list.maxOfOrNull { it * 2 }) // 8
println(list.joinToString(prefix = "[", postfix = "]")) // [1, 2, 3, 4]
println(list.joinToString(limit = 2, truncated = "…")) // 1, 2, …
}
```
`maxBy` против `maxOf` - частая путаница:
```kotlin
data class User8(val name: String, val age: Int)
fun main() {
val users = listOf(User8("Иван", 30), User8("Пётр", 25))
println(users.maxByOrNull { it.age }) // User8(name=Иван, age=30) — элемент
println(users.maxOfOrNull { it.age }) // 30 — значение
}
```
## fold и reduce
```kotlin
fun main() {
val list = listOf(1, 2, 3, 4)
println(list.fold(0) { acc, x -> acc + x }) // 10
println(list.fold(1) { acc, x -> acc * x }) // 24
println(list.reduce { acc, x -> acc + x }) // 10
println(list.runningFold(0) { acc, x -> acc + x }) // [0, 1, 3, 6, 10]
println(list.foldRight(0) { x, acc -> acc * 10 + x }) // 4321 — справа налево
println(emptyList<Int>().fold(0) { a, b -> a + b }) // 0
try {
emptyList<Int>().reduce { a, b -> a + b }
} catch (e: UnsupportedOperationException) {
println("reduce падает на пустой коллекции")
}
}
```
| | `fold` | `reduce` |
|---|---|---|
| начальное значение | задаёте вы | первый элемент |
| тип результата | любой | тип элемента |
| пустая коллекция | вернёт начальное | исключение |
`fold` умеет менять тип, и это его главное применение:
```kotlin
fun main() {
val words = listOf("раз", "два", "три")
val lengths: Map<String, Int> = words.fold(mutableMapOf()) { acc, w ->
acc[w] = w.length
acc
}
println(lengths) // {раз=3, два=3, три=3}
println(words.fold(0) { acc, w -> acc + w.length }) // 9 — List<String> -> Int
}
```
## Группировка
```kotlin
fun main() {
val words = listOf("яблоко", "арбуз", "банан", "боб")
println(words.groupBy { it.first() })
// {я=[яблоко], а=[арбуз], б=[банан, боб]}
println(words.groupBy(keySelector = { it.first() }, valueTransform = { it.length }))
// {я=[6], а=[5], б=[5, 3]}
println(words.groupingBy { it.first() }.eachCount())
// {я=1, а=1, б=2}
println(words.groupingBy { it.first() }.fold(0) { acc, w -> acc + w.length })
// {я=6, а=5, б=8}
}
```
`groupingBy` отличается от `groupBy` тем, что не создаёт промежуточные списки - он сразу агрегирует. Для подсчёта и суммирования по группам это и быстрее, и короче.
## associate
```kotlin
fun main() {
val users = listOf("Иван", "Пётр", "Анна")
println(users.associateWith { it.length }) // {Иван=4, Пётр=4, Анна=4}
println(users.associateBy { it.first() }) // {И=Иван, П=Пётр, А=Анна}
println(users.associate { it.first() to it.length }) // {И=4, П=4, А=4}
}
```
| Функция | Ключ | Значение |
|---|---|---|
| `associateWith` | элемент | результат лямбды |
| `associateBy` | результат лямбды | элемент |
| `associate` | из `Pair` | из `Pair` |
`associateBy` против `groupBy` - важная разница:
```kotlin
fun main() {
val words = listOf("банан", "боб")
println(words.associateBy { it.first() }) // {б=боб} — потеря!
println(words.groupBy { it.first() }) // {б=[банан, боб]}
}
```
`associateBy` при дубликате ключа **молча перезаписывает**. Если ключ не уникален, нужен `groupBy`.
## Сортировка
```kotlin
fun main() {
val list = listOf(3, 1, 2)
println(list.sorted()) // [1, 2, 3]
println(list.sortedDescending()) // [3, 2, 1]
println(list.reversed()) // [2, 1, 3] — не сортировка!
println(list.shuffled().size) // 3
val users = listOf("Пётр" to 25, "Иван" to 30, "Анна" to 25)
println(users.sortedBy { it.second }) // [(Пётр, 25), (Анна, 25), (Иван, 30)]
println(users.sortedWith(compareBy({ it.second }, { it.first })))
// [(Анна, 25), (Пётр, 25), (Иван, 30)] — по возрасту, потом по имени
println(users.sortedWith(compareByDescending<Pair<String, Int>> { it.second }
.thenBy { it.first }))
// [(Иван, 30), (Анна, 25), (Пётр, 25)]
}
```
Сортировка Kotlin **стабильна**: элементы с равными ключами сохраняют исходный порядок.
На `MutableList` есть сортировка на месте - без `ed` в имени:
```kotlin
fun main() {
val list = mutableListOf(3, 1, 2)
list.sort() // на месте
println(list) // [1, 2, 3]
list.sortByDescending { it }
println(list) // [3, 2, 1]
val copy = list.sorted() // новая коллекция
println("$list / $copy") // [3, 2, 1] / [1, 2, 3]
}
```
Соглашение в stdlib: `sort()` меняет на месте, `sorted()` возвращает копию. То же с `reverse()`/`reversed()`, `shuffle()`/`shuffled()`.
## Разбиение и объединение
```kotlin
fun main() {
val list = listOf(1, 2, 3, 4, 5)
println(list.chunked(2)) // [[1, 2], [3, 4], [5]]
println(list.chunked(2) { it.sum() }) // [3, 7, 5]
println(list.windowed(3)) // [[1,2,3], [2,3,4], [3,4,5]]
println(list.windowed(2, step = 2, partialWindows = true)) // [[1,2], [3,4], [5]]
println(list.zipWithNext()) // [(1,2), (2,3), (3,4), (4,5)]
println(list.zipWithNext { a, b -> b - a }) // [1, 1, 1, 1]
}
```
`windowed` - скользящее окно: удобно для расчёта разниц, средних и сглаживания.
## Операции на Map
```kotlin
fun main() {
val map = mapOf("a" to 1, "b" to 2, "c" to 3)
println(map.filter { (_, v) -> v > 1 }) // {b=2, c=3}
println(map.mapValues { it.value * 10 }) // {a=10, b=20, c=30}
println(map.mapKeys { it.key.uppercase() }) // {A=1, B=2, C=3}
println(map.filterKeys { it != "a" }) // {b=2, c=3}
println(map.maxByOrNull { it.value }?.key) // c
println(map.entries.sumOf { it.value }) // 6
println(map.toList().map { (k, v) -> "$k$v" }) // [a1, b2, c3]
}
```
Деструктуризация в лямбде (`{ (k, v) -> }`) работает для `Map.Entry` и для `Pair`.
## Практический пример
```kotlin
data class Sale(val region: String, val product: String, val amount: Long)
fun main() {
val sales = listOf(
Sale("Север", "хлеб", 100),
Sale("Север", "молоко", 200),
Sale("Юг", "хлеб", 150),
Sale("Юг", "сыр", 300),
)
val byRegion = sales.groupBy { it.region }.mapValues { (_, v) -> v.sumOf { it.amount } }
println(byRegion) // {Север=300, Юг=450}
val top = sales.maxByOrNull { it.amount }
println("${top?.product}: ${top?.amount}") // сыр: 300
val products = sales.map { it.product }.distinct().sorted()
println(products) // [молоко, сыр, хлеб]
val report = byRegion.entries
.sortedByDescending { it.value }
.joinToString("\n") { "${it.key}: ${it.value}" }
println(report) // Юг: 450\nСевер: 300
}
```
> [!info] Отличия от Java
> | Java Stream | Kotlin |
> |---|---|
> | `stream().map(f).collect(toList())` | `map(f)` |
> | `stream().filter(p).collect(toList())` | `filter(p)` |
> | `collect(groupingBy(f))` | `groupBy(f)` |
> | `collect(toMap(k, v))` | `associate { k to v }` |
> | `reduce(0, Integer::sum)` | `fold(0) { a, b -> a + b }` |
> | `mapToInt(f).sum()` | `sumOf(f)` |
> | `flatMap(x -> x.stream())` | `flatMap { it }` |
> | `collect(partitioningBy(p))` | `partition(p)` |
> | `anyMatch(p)` | `any(p)` |
> | нет аналога | `mapNotNull`, `windowed`, `zipWithNext`, `groupingBy` |
>
> **Главное - не нужен `.stream()` и `.collect()`.** В Java любая операция требует входа в стрим и выхода из него; это ~25 символов обвязки на каждую цепочку. В Kotlin операции объявлены прямо на `Iterable`, поэтому короткие цепочки читаются как одно выражение:
> ```kotlin
> fun main() {
> println(listOf(1, 2, 3).filter { it > 1 }.sumOf { it * 10 }) // 50
> }
> ```
> ```java
> List.of(1, 2, 3).stream().filter(x -> x > 1).mapToInt(x -> x * 10).sum();
> ```
>
> **Цена - энергичность.** Java-стрим ленив: промежуточные операции не создают коллекций. Kotlin-операции на `Iterable` создают новый список на каждом шаге. Для цепочки из 3 операций по списку из 1000 элементов это 3 списка. Ленивый аналог - [Sequence](Sequence.md), и он почти дословно соответствует `Stream`.
>
> **`mapNotNull` и `groupingBy` аналогов в Java не имеют.** В Java фильтрация с преобразованием - это `map` + `filter(Objects::nonNull)` или `flatMap` с `Optional.stream()`. Подсчёт по группам - `groupingBy(f, counting())`, то есть вложенные коллекторы, которые плохо читаются.
>
> **`Optional` в цепочках не нужен.** Java-стримы возвращают `Optional` из `findFirst` и `max`; в Kotlin это nullable-тип, с которым работают обычные `?.` и `?:`:
> ```kotlin
> fun main() {
> println(listOf(1, 2, 3).maxOrNull() ?: 0) // 3
> }
> ```
>
> Сравнить: [Функции в Java](../../Программирование%20на%20Java/Синтаксис%20и%20типы/Работа%20с%20данными%20и%20типами/Функции.md)
> [!warning] Подводные камни
> **Каждая операция создаёт новую коллекцию:**
> ```kotlin
> fun main() {
> val big = List(100_000) { it }
> val result = big.map { it * 2 }.filter { it % 3 == 0 }.take(5)
> println(result) // [0, 6, 12, 18, 24]
> }
> ```
> `map` обработал все 100 000 элементов и создал список, `filter` - ещё один, и только потом `take` взял пять. С `asSequence()` работы было бы на порядки меньше.
>
> **`all` на пустой коллекции - `true`:**
> ```kotlin
> fun main() {
> val errors = emptyList<String>()
> println(errors.all { it.isNotBlank() }) // true
> println(errors.isNotEmpty() && errors.all { it.isNotBlank() }) // false
> }
> ```
> Проверка «все данные валидны» на пустом входе пройдёт. Если пустота недопустима, проверяйте её отдельно.
>
> **`associateBy` теряет дубликаты молча** - при повторе ключа остаётся последний элемент. Для неуникальных ключей нужен `groupBy`.
>
> **`reduce` падает на пустой коллекции**, `fold` - нет. В коде, где коллекция может быть пустой, `reduce` - потенциальное исключение.
>
> **`sortedBy` с nullable-ключом ставит `null` первыми:**
> ```kotlin
> fun main() {
> val list = listOf("b" to 2, "a" to null, "c" to 1)
> println(list.sortedBy { it.second }) // [(a, null), (c, 1), (b, 2)]
> println(list.sortedWith(compareBy(nullsLast()) { it.second })) // [(c, 1), (b, 2), (a, null)]
> }
> ```
>
> **`reversed()` - не сортировка**, а переворот. `listOf(3, 1, 2).reversed()` даёт `[2, 1, 3]`.
>
> **`sort()` и `sorted()` легко перепутать.** `sort()` молча меняет исходную коллекцию - если она пришла извне, это неожиданная мутация:
> ```kotlin
> fun process(items: MutableList<Int>): List<Int> {
> items.sort() // испортили аргумент вызывающего
> return items
> }
>
> fun main() {
> val original = mutableListOf(3, 1, 2)
> process(original)
> println(original) // [1, 2, 3] — вызывающий этого не просил
> }
> ```
> Принимайте `List` вместо `MutableList` - тогда `sort()` просто не скомпилируется.
>
> **`first()` и `single()` бросают, а не возвращают `null`.** Версии с `OrNull` безопасны; `single` вдобавок падает, если элементов больше одного.
>
> **`flatMap { it }` на `List<List<T>>` работает, но `flatten()` яснее.**
>
> **Цепочка из `filter` после `map` делает лишнюю работу.** `filter` перед `map` обрабатывает меньше элементов:
> ```kotlin
> fun main() {
> val list = List(10) { it }
> println(list.map { it * 2 }.filter { it > 10 }) // [12, 14, 16, 18], 10 преобразований
> println(list.filter { it > 5 }.map { it * 2 }) // [12, 14, 16, 18], 4 преобразования
> }
> ```
> Здесь результаты совпадают, а работы во втором случае вдвое с лишним меньше. Но совпадают они не всегда: перестановка `filter` и `map` меняет смысл, если предикат проверяет уже преобразованное значение. Сначала убедитесь в эквивалентности, потом выбирайте по стоимости.
>
> **`joinToString` по умолчанию без ограничения длины.** Для списка на миллион элементов он построит огромную строку; в логах используйте `limit`.
Далее: [Sequence](Sequence.md)