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

235 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Коллекции
description: Обзор иерархии коллекций Kotlin, разделение на читаемые и изменяемые интерфейсы
tags:
- kotlin
- collections
---
Коллекции Kotlin - это **те же** классы JDK (`ArrayList`, `HashMap`, `LinkedHashSet`), но с другими интерфейсами поверх них. Главное отличие от Java: изменяемость выражена в системе типов.
```kotlin
fun main() {
val readOnly: List<Int> = listOf(1, 2, 3)
val mutable: MutableList<Int> = mutableListOf(1, 2, 3)
mutable.add(4)
// readOnly.add(4) // ошибка: у List нет метода add
println("$readOnly / $mutable") // [1, 2, 3] / [1, 2, 3, 4]
println(readOnly.javaClass.name) // java.util.Arrays$ArrayList
println(mutable.javaClass.name) // java.util.ArrayList
}
```
## Иерархия
```mermaid
graph TD
I["Iterable&lt;out T&gt;"] --> C["Collection&lt;out T&gt;"]
C --> L["List&lt;out T&gt;"]
C --> S["Set&lt;out T&gt;"]
I --> MI["MutableIterable&lt;T&gt;"]
MI --> MC["MutableCollection&lt;T&gt;"]
C --> MC
MC --> ML["MutableList&lt;T&gt;"]
L --> ML
MC --> MS["MutableSet&lt;T&gt;"]
S --> MS
M["Map&lt;K, out V&gt;"] --> MM["MutableMap&lt;K, V&gt;"]
```
Два важных факта из этой схемы:
- `MutableList` **наследует** `List`, то есть изменяемую коллекцию можно передать туда, где ожидается читаемая. Обратное невозможно.
- `Map` не входит в `Collection` - это отдельная иерархия, как и в Java.
| Интерфейс | Чтение | Изменение | Реализация по умолчанию |
|---|---|---|---|
| `List<T>` | да | нет | `ArrayList` под капотом |
| `MutableList<T>` | да | да | `ArrayList` |
| `Set<T>` | да | нет | `LinkedHashSet` |
| `MutableSet<T>` | да | да | `LinkedHashSet` |
| `Map<K, V>` | да | нет | `LinkedHashMap` |
| `MutableMap<K, V>` | да | да | `LinkedHashMap` |
Обратите внимание: умолчание для `Set` и `Map` - **`Linked`**-версии, то есть порядок вставки сохраняется. В Java `new HashSet<>()` порядок не гарантирует.
## Создание
```kotlin
fun main() {
val a = listOf(1, 2, 3) // read-only
val b = mutableListOf(1, 2, 3) // изменяемый
val c = emptyList<Int>() // пустой
val d = listOfNotNull(1, null, 3) // null отфильтрованы
val e = List(3) { it * it } // по индексу
println("$a $b $c $d $e") // [1, 2, 3] [1, 2, 3] [] [1, 3] [0, 1, 4]
val s = setOf(1, 2, 2, 3) // дубликаты убраны
val m = mapOf("a" to 1, "b" to 2)
println("$s $m") // [1, 2, 3] {a=1, b=2}
}
```
Специализированные варианты для разных реализаций:
```kotlin
fun main() {
val sorted = sortedSetOf(3, 1, 2) // TreeSet
val sortedMap = sortedMapOf("b" to 2, "a" to 1)
val linked = linkedSetOf(3, 1, 2) // LinkedHashSet
val hash = hashSetOf(3, 1, 2) // HashSet, порядок не гарантирован
val arrayDeque = ArrayDeque(listOf(1, 2, 3))
println("$sorted $sortedMap $linked") // [1, 2, 3] {a=1, b=2} [3, 1, 2]
println(hash.size) // 3
arrayDeque.addFirst(0)
println(arrayDeque) // [0, 1, 2, 3]
}
```
## Билдеры
Когда коллекцию нужно собрать по условию, а результат отдать как read-only:
```kotlin
fun main() {
val list = buildList {
add(1)
if (true) add(2)
addAll(listOf(3, 4))
}
println(list) // [1, 2, 3, 4]
val map = buildMap {
put("a", 1)
putAll(mapOf("b" to 2))
}
println(map) // {a=1, b=2}
val set = buildSet { repeat(3) { add(it % 2) } }
println(set) // [0, 1]
}
```
Внутри лямбды коллекция изменяемая, снаружи возвращается `List<T>`. Это чище, чем `mutableListOf` с последующим приведением.
## Пустая коллекция и null
```kotlin
fun main() {
val nullable: List<Int>? = null
println(nullable?.size ?: 0) // 0
println(nullable.orEmpty()) // []
println(nullable.isNullOrEmpty()) // true
val empty = emptyList<Int>()
println(empty.firstOrNull()) // null
println(empty.sum()) // 0
}
```
Практическое правило: в API возвращайте пустую коллекцию, а не `null`. `List<T>?` заставляет каждого клиента писать `?.` или `orEmpty()`, не давая взамен никакой информации - пустой список уже означает «ничего нет».
## Массив не коллекция
```kotlin
fun main() {
val array = arrayOf(1, 2, 3)
// array.filter — есть, потому что Array получает расширения из Iterable
println(array.filter { it > 1 }) // [2, 3] — это уже List
println(array.toList()::class.simpleName) // ArrayList
println(listOf(1, 2).toTypedArray().size) // 2
}
```
`Array<T>` - отдельный тип, не входящий в `Collection`. У него фиксированный размер и инвариантность. Подробнее: [Массивы](../Синтаксис%20и%20типы/Работа%20с%20данными/Массивы.md)
## Карта раздела
| Заметка | О чём |
|---|---|
| [List, Set, Map](List,%20Set,%20Map.md) | три семейства, их гарантии и выбор между ними |
| [Изменяемость](Изменяемость.md) | read-only против immutable, защита от изменения |
| [Операции](Операции.md) | `map`, `filter`, `groupBy`, `fold` и остальной арсенал |
| [Sequence](Sequence.md) | ленивые вычисления и когда они выигрывают |
| [Коллекции под капотом](Коллекции%20под%20капотом.md) | что лежит в байт-коде, сложность операций, память |
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `new ArrayList<>()` | `mutableListOf()` |
> | `List.of(1, 2)` (Java 9+) | `listOf(1, 2)` |
> | `Collections.emptyList()` | `emptyList()` |
> | изменяемость не в типе | `List` против `MutableList` |
> | `UnsupportedOperationException` в рантайме | ошибка компиляции |
> | `Stream` для операций | расширения прямо на коллекции |
> | `new HashSet<>()` - порядок не гарантирован | `setOf()` - порядок вставки |
>
> **Разделение на `List` и `MutableList` - главное нововведение.** В Java `List` - один интерфейс с методом `add`, и `List.of(1, 2)` его тоже имеет: вызов просто бросает `UnsupportedOperationException` в рантайме.
> ```java
> List<Integer> list = List.of(1, 2);
> list.add(3); // компилируется, падает в рантайме
> ```
> ```kotlin
> fun main() {
> val list = listOf(1, 2)
> // list.add(3) // не компилируется
> println(list.size)
> }
> ```
> Ошибка переехала из рантайма в компиляцию - это главная практическая польза.
>
> **Операции не требуют `Stream`.** В Java цепочка выглядит как `list.stream().filter(...).map(...).collect(Collectors.toList())`. В Kotlin `filter` и `map` - функции-расширения прямо на `Iterable`, они сразу возвращают `List`:
> ```kotlin
> fun main() {
> println(listOf(1, 2, 3).filter { it > 1 }.map { it * 10 }) // [20, 30]
> }
> ```
> Цена - промежуточный список на каждом шаге. Ленивый аналог `Stream` - это [Sequence](Sequence.md).
>
> **`setOf` и `mapOf` сохраняют порядок вставки**, потому что под ними `LinkedHashSet` и `LinkedHashMap`. Это удобно (вывод предсказуем), но на порядок всё равно не стоит полагаться в контракте: тип объявлен как `Set`, а не как «упорядоченное множество».
>
> Сравнить: [HashMap в Java](../../Программирование%20на%20Java/Коллекции/HashMap.md)
> [!warning] Подводные камни
> **`List` - это read-only, а не immutable.** Объект под ним может меняться через другую ссылку:
> ```kotlin
> fun main() {
> val mutable = mutableListOf(1, 2)
> val readOnly: List<Int> = mutable // та же коллекция!
> mutable.add(3)
> println(readOnly) // [1, 2, 3] — изменился
> }
> ```
> `List` гарантирует только то, что **вы** через эту ссылку не измените коллекцию. Подробнее: [Изменяемость](Изменяемость.md)
>
> **Из Java любой `List` изменяем.** Типы Kotlin-коллекций - это `java.util.List`, поэтому Java-код про read-only не знает:
> ```kotlin
> fun giveList(): List<Int> = mutableListOf(1, 2)
>
> fun main() { println(giveList().size) } // 2
> ```
> Java-клиент вызовет `add` без всяких ошибок компиляции, и если под ссылкой `ArrayList` - элемент добавится. Для настоящей защиты нужна копия.
>
> **`listOf()` возвращает разные реализации.** `listOf()` без аргументов - `EmptyList`, `listOf(x)` - `SingletonList`, с несколькими - `Arrays$ArrayList`. То же у `mapOf` и `setOf`: на одном элементе это `SingletonMap`/`SingletonSet`, а не `Linked`-версия. Последний не поддерживает `add` даже через каст:
> ```kotlin
> fun main() {
> val list = listOf(1, 2)
> try {
> @Suppress("UNCHECKED_CAST")
> (list as MutableList<Int>).add(3)
> } catch (e: UnsupportedOperationException) {
> println("каст скомпилировался, но add упал")
> }
> }
> ```
> Приводить `List` к `MutableList` нельзя: иногда это сработает, иногда бросит исключение.
>
> **`Map` не `Collection`** - у него нет `size` через `Collection`-интерфейс, `filter` работает с `Map.Entry`, а `map` возвращает `List`:
> ```kotlin
> fun main() {
> val m = mapOf("a" to 1, "b" to 2)
> println(m.filter { it.value > 1 }) // {b=2} — остался Map
> println(m.map { it.key }) // [a, b] — стал List
> println(m.mapValues { it.value * 10 }) // {a=10, b=20}
> }
> ```
>
> **Операции создают новую коллекцию на каждом шаге.** Цепочка из пяти `map`/`filter` по списку в миллион элементов создаст пять списков. Для длинных цепочек и больших данных нужен [Sequence](Sequence.md).
>
> **`hashSetOf` и `hashMapOf` не гарантируют порядок.** Если тест полагается на порядок обхода, он может сломаться при смене JDK. Используйте `setOf`/`mapOf` или сортируйте явно.
Далее: [List, Set, Map](List,%20Set,%20Map.md)