235 lines
12 KiB
Markdown
235 lines
12 KiB
Markdown
---
|
||
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)
|