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
@@ -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)