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

12 KiB
Raw Blame History

title, description, tags
title description tags
Коллекции Обзор иерархии коллекций Kotlin, разделение на читаемые и изменяемые интерфейсы
kotlin
collections

Коллекции Kotlin - это те же классы JDK (ArrayList, HashMap, LinkedHashSet), но с другими интерфейсами поверх них. Главное отличие от Java: изменяемость выражена в системе типов.

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
}

Иерархия

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<>() порядок не гарантирует.

Создание

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}
}

Специализированные варианты для разных реализаций:

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:

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

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(), не давая взамен никакой информации - пустой список уже означает «ничего нет».

Массив не коллекция

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. У него фиксированный размер и инвариантность. Подробнее: Массивы

Карта раздела

Заметка О чём
List, Set, Map три семейства, их гарантии и выбор между ними
Изменяемость read-only против immutable, защита от изменения
Операции map, filter, groupBy, fold и остальной арсенал
Sequence ленивые вычисления и когда они выигрывают
Коллекции под капотом что лежит в байт-коде, сложность операций, память

[!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 в рантайме.

List<Integer> list = List.of(1, 2);
list.add(3);        // компилируется, падает в рантайме
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:

fun main() {
    println(listOf(1, 2, 3).filter { it > 1 }.map { it * 10 })    // [20, 30]
}

Цена - промежуточный список на каждом шаге. Ленивый аналог Stream - это Sequence.

setOf и mapOf сохраняют порядок вставки, потому что под ними LinkedHashSet и LinkedHashMap. Это удобно (вывод предсказуем), но на порядок всё равно не стоит полагаться в контракте: тип объявлен как Set, а не как «упорядоченное множество».

Сравнить: HashMap в Java

[!warning] Подводные камни List - это read-only, а не immutable. Объект под ним может меняться через другую ссылку:

fun main() {
    val mutable = mutableListOf(1, 2)
    val readOnly: List<Int> = mutable      // та же коллекция!
    mutable.add(3)
    println(readOnly)                       // [1, 2, 3] — изменился
}

List гарантирует только то, что вы через эту ссылку не измените коллекцию. Подробнее: Изменяемость

Из Java любой List изменяем. Типы Kotlin-коллекций - это java.util.List, поэтому Java-код про read-only не знает:

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 даже через каст:

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:

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.

hashSetOf и hashMapOf не гарантируют порядок. Если тест полагается на порядок обхода, он может сломаться при смене JDK. Используйте setOf/mapOf или сортируйте явно.

Далее: List, Set, Map