Kotlin guide done. (20.10.26)
This commit is contained in:
@@ -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)
|
||||
@@ -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<out T>"] --> C["Collection<out T>"]
|
||||
C --> L["List<out T>"]
|
||||
C --> S["Set<out T>"]
|
||||
I --> MI["MutableIterable<T>"]
|
||||
MI --> MC["MutableCollection<T>"]
|
||||
C --> MC
|
||||
MC --> ML["MutableList<T>"]
|
||||
L --> ML
|
||||
MC --> MS["MutableSet<T>"]
|
||||
S --> MS
|
||||
M["Map<K, out V>"] --> MM["MutableMap<K, V>"]
|
||||
```
|
||||
|
||||
Два важных факта из этой схемы:
|
||||
- `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)
|
||||
Reference in New Issue
Block a user