16 KiB
title, description, tags
| title | description | tags | ||
|---|---|---|---|---|
| List, Set, Map | Три семейства коллекций, их гарантии, стоимость операций и выбор между ними |
|
List |
Set |
Map |
|
|---|---|---|---|
| порядок | по индексу | вставки (у LinkedHashSet) |
вставки (у LinkedHashMap) |
| дубликаты | разрешены | запрещены | ключи уникальны |
| доступ | по индексу, O(1) | только поиск, O(1) | по ключу, O(1) |
| поиск элемента | O(n) | O(1) | O(1) по ключу |
| основной вопрос | «что по порядку» | «есть ли такой» | «что соответствует» |
List
fun main() {
val list = listOf("a", "b", "c", "b")
println(list[1]) // b — доступ по индексу
println(list.getOrNull(10)) // null
println(list.getOrElse(10) { "?" }) // ?
println(list.indexOf("b")) // 1 — первое вхождение
println(list.lastIndexOf("b")) // 3
println(list.subList(1, 3)) // [b, c]
println(list.size) // 4 — дубликат сохранён
}
Доступ к краям и срезы:
fun main() {
val list = listOf(1, 2, 3, 4, 5)
println("${list.first()} ${list.last()}") // 1 5
println(list.firstOrNull { it > 3 }) // 4
println(list.take(2)) // [1, 2]
println(list.drop(2)) // [3, 4, 5]
println(list.takeLast(2)) // [4, 5]
println(list.slice(1..3)) // [2, 3, 4]
println(list.chunked(2)) // [[1, 2], [3, 4], [5]]
println(list.windowed(2)) // [[1, 2], [2, 3], [3, 4], [4, 5]]
println(list.reversed()) // [5, 4, 3, 2, 1]
}
Изменение через MutableList:
fun main() {
val list = mutableListOf(1, 2, 3)
list.add(4)
list.add(0, 0) // по индексу
list[1] = 10
list.removeAt(0)
list.remove(4) // по значению
println(list) // [10, 2, 3]
list += 5 // оператор += работает как add
list -= 2
println(list) // [10, 3, 5]
list.sortDescending() // сортировка на месте
println(list) // [10, 5, 3]
}
Set
fun main() {
val set = setOf(1, 2, 3)
println(2 in set) // true — O(1)
println(set.contains(5)) // false
println(setOf(1, 2, 2, 3).size) // 3 — дубликат отброшен
}
Операции над множествами:
fun main() {
val a = setOf(1, 2, 3)
val b = setOf(3, 4, 5)
println(a union b) // [1, 2, 3, 4, 5]
println(a intersect b) // [3]
println(a subtract b) // [1, 2]
println(a + b) // [1, 2, 3, 4, 5] — как union
println(a - b) // [1, 2]
}
Эти же функции работают на List, но результат всегда Set:
fun main() {
println(listOf(1, 1, 2) union listOf(2, 3)) // [1, 2, 3]
println(listOf(1, 1, 2).distinct()) // [1, 2] — а так останется List
}
Три реализации Set
fun main() {
val linked = linkedSetOf(3, 1, 2) // порядок вставки
val hash = hashSetOf(3, 1, 2) // порядок не определён
val sorted = sortedSetOf(3, 1, 2) // отсортирован
println(linked) // [3, 1, 2]
println(sorted) // [1, 2, 3]
println(hash.size) // 3
}
| Реализация | Порядок | Поиск | Когда |
|---|---|---|---|
LinkedHashSet (setOf) |
вставки | O(1) | по умолчанию |
HashSet (hashSetOf) |
не определён | O(1) | экономия памяти, порядок не нужен |
TreeSet (sortedSetOf) |
сортировки | O(log n) | нужен отсортированный обход |
Элементы Set обязаны корректно реализовывать equals и hashCode - иначе множество работает неправильно. Подробнее: equals, hashCode и toString
Map
fun main() {
val map = mapOf("a" to 1, "b" to 2)
println(map["a"]) // 1
println(map["z"]) // null
println(map.getOrDefault("z", 0)) // 0
println(map.getOrElse("z") { -1 }) // -1
println(map.getValue("a")) // 1, бросает при отсутствии
println("a" in map) // true — проверка ключа
println(map.containsValue(2)) // true
println("${map.keys} ${map.values}") // [a, b] [1, 2]
}
to - инфиксная функция, создающая Pair:
fun main() {
val pair: Pair<String, Int> = "a" to 1
println("${pair.first} ${pair.second}") // a 1
val (k, v) = pair // деструктуризация
println("$k=$v") // a=1
println(listOf("a" to 1, "b" to 2).toMap()) // {a=1, b=2}
}
Изменение через MutableMap:
fun main() {
val map = mutableMapOf("a" to 1)
map["b"] = 2
map.put("c", 3)
map.putAll(mapOf("d" to 4))
map.remove("a")
println(map) // {b=2, c=3, d=4}
println(map.getOrPut("e") { 5 }) // 5 — вычислит и положит
println(map.getOrPut("e") { 99 }) // 5 — уже есть, лямбда не вызвана
map.merge("b", 10) { old, new -> old + new } // метод JDK
println(map) // {b=12, c=3, d=4, e=5}
}
getOrPut - основной инструмент кеширования: одна операция вместо проверки и вставки.
Обход:
fun main() {
val map = mapOf("a" to 1, "b" to 2)
for ((key, value) in map) print("$key=$value ") // a=1 b=2
println()
map.forEach { (key, value) -> print("$key$value ") } // a1 b2
println()
println(map.entries.first().key) // a
}
Деструктуризация в for и в лямбде - главная причина, почему обход Map в Kotlin короче, чем в Java.
Преобразования:
fun main() {
val map = mapOf("a" to 1, "b" to 2, "c" to 3)
println(map.filter { it.value > 1 }) // {b=2, c=3}
println(map.filterKeys { it != "a" }) // {b=2, c=3}
println(map.mapValues { it.value * 10 }) // {a=10, b=20, c=30}
println(map.mapKeys { it.key.uppercase() }) // {A=1, B=2, C=3}
println(map.map { "${it.key}${it.value}" }) // [a1, b2, c3] — это List
println(map.toList()) // [(a, 1), (b, 2), (c, 3)]
println(map.entries.sortedByDescending { it.value }.first().key) // c
}
Три реализации Map
| Реализация | Порядок | Доступ | Когда |
|---|---|---|---|
LinkedHashMap (mapOf) |
вставки | O(1) | по умолчанию |
HashMap (hashMapOf) |
не определён | O(1) | экономия памяти |
TreeMap (sortedMapOf) |
сортировки ключей | O(log n) | диапазонные запросы |
Выбор структуры
graph TD
A["Нужна коллекция"] --> B{"Пары ключ-значение?"}
B -->|да| M["Map"]
B -->|нет| C{"Дубликаты важны<br/>или нужен индекс?"}
C -->|да| L["List"]
C -->|нет| D{"Частый поиск<br/>элемента?"}
D -->|да| S["Set"]
D -->|нет| L
Практический критерий: если в коде есть list.contains(x) внутри цикла - это Set. Такая замена превращает O(n²) в O(n):
fun main() {
val ids = List(1000) { it }
val allowedList = List(1000) { it * 2 }
val allowedSet = allowedList.toSet()
println(ids.count { it in allowedList }) // O(n*m) — 1000*1000 сравнений
println(ids.count { it in allowedSet }) // O(n) — то же 500, но быстро
}
Специальные коллекции
fun main() {
val deque = ArrayDeque(listOf(2, 3))
deque.addFirst(1)
deque.addLast(4)
println("$deque ${deque.removeFirst()} ${deque.removeLast()}") // [1, 2, 3, 4] 1 4
val grouped = listOf("яблоко", "арбуз", "банан").groupBy { it.first() }
println(grouped) // {я=[яблоко], а=[арбуз], б=[банан]}
val assoc = listOf("a", "bb", "ccc").associateWith { it.length }
println(assoc) // {a=1, bb=2, ccc=3}
}
ArrayDeque в Kotlin - своя реализация (не java.util.ArrayDeque), работает как стек и как очередь. Подробнее об groupBy и associate: Операции
[!info] Отличия от Java
Java Kotlin map.get(k)возвращаетVmap[k]возвращаетV?map.put(k, v)map[k] = vnew AbstractMap.SimpleEntry<>(k, v)k to vfor (Map.Entry<K,V> e : map.entrySet())for ((k, v) in map)map.computeIfAbsent(k, f)map.getOrPut(k) { }Collections.unmodifiableListтип Listset.retainAll(other)a intersect blist.get(i)list[i]
map[k]возвращает nullable - и это правильно. В Javamap.get(k)имеет типV, поэтомуint x = map.get("нет")компилируется и даёт NPE при распаковке. Kotlin делает отсутствие ключа видимым в типе:fun main() { val map = mapOf("a" to 1) val x: Int = map["z"] ?: 0 // компилятор требует обработать null println(x) // 0 }Обратная сторона:
map[k]!!появляется в коде часто. Когда ключ гарантированно есть, честнееgetValue(k)- он броситNoSuchElementExceptionс внятным сообщением.Деструктуризация
Map.Entry. Java требуетe.getKey()/e.getValue(); в Kotlinfor ((k, v) in map)работает потому, что дляMap.Entryобъявлены функции-расширенияcomponent1иcomponent2. Подробнее: ДеструктуризацияОперации над множествами как инфиксные функции. В Java
union- этоaddAllна копии,intersect-retainAll, то есть операции мутирующие и требуют ручного копирования. В Kotlina union bвозвращает новое множество и читается как математическая запись.Сравнить: HashMap в Java
[!warning] Подводные камни
map[k]не различает «нет ключа» и «значение null»:fun main() { val map = mapOf("a" to null, "b" to 1) println(map["a"]) // null println(map["z"]) // null — то же самое! println("a" in map) // true println("z" in map) // false — вот так различать }
getValueбросает,get- нет. Это легко перепутать:fun main() { val map = mapOf("a" to 1) println(map["z"]) // null try { map.getValue("z") } catch (e: NoSuchElementException) { println("getValue бросает") // getValue бросает } }Изменяемый элемент в
Setили ключ вMapломает коллекцию:data class Key(var id: Int) fun main() { val key = Key(1) val set = mutableSetOf(key) println(key in set) // true key.id = 2 // hashCode изменился! println(key in set) // false — элемент потерян println(set.size) // 1 — но он всё ещё внутри }Ключи и элементы множеств обязаны быть неизменяемыми.
data classсvarв этой роли - ошибка.
subList- это вид на исходный список, а не копия:fun main() { val list = mutableListOf(1, 2, 3, 4) val sub = list.subList(1, 3) list[1] = 99 println(sub) // [99, 3] — изменился list.add(5) try { println(sub) } catch (e: ConcurrentModificationException) { println("структурное изменение списка сломало subList") } }Для независимой копии нужен
sliceилиtoList().
removeнаMutableList<Int>неоднозначен:fun main() { val list = mutableListOf(10, 20, 30) list.removeAt(1) // по индексу println(list) // [10, 30] list.remove(10) // по значению println(list) // [30] }В Kotlin, в отличие от Java, перегрузки по индексу у
removeнет - толькоremoveAt. Это как раз устраняет классическую Java-ловушкуlist.remove(1).
list.contains(x)в цикле - это O(n²). Самая частая проблема производительности с коллекциями. Если проверка повторяется, предварительно постройтеSet.
hashSetOfиhashMapOfне сохраняют порядок - вывод может отличаться между запусками и версиями JDK. Тест, завязанный на порядок, будет нестабильным.
sortedSetOfтребуетComparableили компаратор и броситClassCastExceptionпри вставке несравнимого элемента - в рантайме, а не при компиляции.
+=наListработает, но создаёт новую коллекцию:fun main() { var list = listOf(1, 2) // var! list += 3 // новый список, не add println(list) // [1, 2, 3] val mutable = mutableListOf(1, 2) mutable += 3 // здесь настоящий add println(mutable) // [1, 2, 3] }На
val list: List<Int>оператор+=не скомпилируется. В цикле по большому списку+=на read-only коллекции даёт O(n²) копирований.
Далее: Изменяемость