12 KiB
title, description, tags
| title | description | tags | ||
|---|---|---|---|---|
| Коллекции | Обзор иерархии коллекций Kotlin, разделение на читаемые и изменяемые интерфейсы |
|
Коллекции 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<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<>() порядок не гарантирует.
Создание
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противMutableListUnsupportedOperationExceptionв рантаймеошибка компиляции Streamдля операцийрасширения прямо на коллекции new HashSet<>()- порядок не гарантированsetOf()- порядок вставкиРазделение на
ListиMutableList- главное нововведение. В JavaList- один интерфейс с методом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()). В Kotlinfilterи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) } // 2Java-клиент вызовет
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