--- title: Коллекции description: Обзор иерархии коллекций Kotlin, разделение на читаемые и изменяемые интерфейсы tags: - kotlin - collections --- Коллекции Kotlin - это **те же** классы JDK (`ArrayList`, `HashMap`, `LinkedHashSet`), но с другими интерфейсами поверх них. Главное отличие от Java: изменяемость выражена в системе типов. ```kotlin fun main() { val readOnly: List = listOf(1, 2, 3) val mutable: MutableList = 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` | да | нет | `ArrayList` под капотом | | `MutableList` | да | да | `ArrayList` | | `Set` | да | нет | `LinkedHashSet` | | `MutableSet` | да | да | `LinkedHashSet` | | `Map` | да | нет | `LinkedHashMap` | | `MutableMap` | да | да | `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() // пустой 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`. Это чище, чем `mutableListOf` с последующим приведением. ## Пустая коллекция и null ```kotlin fun main() { val nullable: List? = null println(nullable?.size ?: 0) // 0 println(nullable.orEmpty()) // [] println(nullable.isNullOrEmpty()) // true val empty = emptyList() println(empty.firstOrNull()) // null println(empty.sum()) // 0 } ``` Практическое правило: в API возвращайте пустую коллекцию, а не `null`. `List?` заставляет каждого клиента писать `?.` или `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` - отдельный тип, не входящий в `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 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 = mutable // та же коллекция! > mutable.add(3) > println(readOnly) // [1, 2, 3] — изменился > } > ``` > `List` гарантирует только то, что **вы** через эту ссылку не измените коллекцию. Подробнее: [Изменяемость](Изменяемость.md) > > **Из Java любой `List` изменяем.** Типы Kotlin-коллекций - это `java.util.List`, поэтому Java-код про read-only не знает: > ```kotlin > fun giveList(): List = 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).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)