--- title: List, Set, Map description: Три семейства коллекций, их гарантии, стоимость операций и выбор между ними tags: - kotlin - collections --- | | `List` | `Set` | `Map` | |---|---|---|---| | порядок | по индексу | вставки (у `LinkedHashSet`) | вставки (у `LinkedHashMap`) | | дубликаты | разрешены | запрещены | ключи уникальны | | доступ | по индексу, O(1) | только поиск, O(1) | по ключу, O(1) | | поиск элемента | O(n) | O(1) | O(1) по ключу | | основной вопрос | «что по порядку» | «есть ли такой» | «что соответствует» | ## List ```kotlin 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 — дубликат сохранён } ``` Доступ к краям и срезы: ```kotlin 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`: ```kotlin 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 ```kotlin 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 — дубликат отброшен } ``` Операции над множествами: ```kotlin 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`: ```kotlin fun main() { println(listOf(1, 1, 2) union listOf(2, 3)) // [1, 2, 3] println(listOf(1, 1, 2).distinct()) // [1, 2] — а так останется List } ``` ### Три реализации Set ```kotlin 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](../Синтаксис%20и%20типы/Базовые%20типы%20ООП/equals,%20hashCode%20и%20toString.md) ## Map ```kotlin 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`: ```kotlin fun main() { val pair: Pair = "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`: ```kotlin 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` - основной инструмент кеширования: одна операция вместо проверки и вставки. Обход: ```kotlin 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. Преобразования: ```kotlin 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) | диапазонные запросы | ## Выбор структуры ```mermaid graph TD A["Нужна коллекция"] --> B{"Пары ключ-значение?"} B -->|да| M["Map"] B -->|нет| C{"Дубликаты важны
или нужен индекс?"} C -->|да| L["List"] C -->|нет| D{"Частый поиск
элемента?"} D -->|да| S["Set"] D -->|нет| L ``` Практический критерий: если в коде есть `list.contains(x)` внутри цикла - это `Set`. Такая замена превращает O(n²) в O(n): ```kotlin 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, но быстро } ``` ## Специальные коллекции ```kotlin 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`: [Операции](Операции.md) > [!info] Отличия от Java > | Java | Kotlin | > |---|---| > | `map.get(k)` возвращает `V` | `map[k]` возвращает `V?` | > | `map.put(k, v)` | `map[k] = v` | > | `new AbstractMap.SimpleEntry<>(k, v)` | `k to v` | > | `for (Map.Entry e : map.entrySet())` | `for ((k, v) in map)` | > | `map.computeIfAbsent(k, f)` | `map.getOrPut(k) { }` | > | `Collections.unmodifiableList` | тип `List` | > | `set.retainAll(other)` | `a intersect b` | > | `list.get(i)` | `list[i]` | > > **`map[k]` возвращает nullable - и это правильно.** В Java `map.get(k)` имеет тип `V`, поэтому `int x = map.get("нет")` компилируется и даёт NPE при распаковке. Kotlin делает отсутствие ключа видимым в типе: > ```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()`; в Kotlin `for ((k, v) in map)` работает потому, что для `Map.Entry` объявлены функции-расширения `component1` и `component2`. Подробнее: [Деструктуризация](../Синтаксис%20и%20типы/Работа%20с%20данными/Деструктуризация.md) > > **Операции над множествами как инфиксные функции.** В Java `union` - это `addAll` на копии, `intersect` - `retainAll`, то есть операции мутирующие и требуют ручного копирования. В Kotlin `a union b` возвращает новое множество и читается как математическая запись. > > Сравнить: [HashMap в Java](../../Программирование%20на%20Java/Коллекции/HashMap.md) > [!warning] Подводные камни > **`map[k]` не различает «нет ключа» и «значение null»:** > ```kotlin > 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` - нет.** Это легко перепутать: > ```kotlin > fun main() { > val map = mapOf("a" to 1) > println(map["z"]) // null > try { > map.getValue("z") > } catch (e: NoSuchElementException) { > println("getValue бросает") // getValue бросает > } > } > ``` > > **Изменяемый элемент в `Set` или ключ в `Map` ломает коллекцию:** > ```kotlin > 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` - это вид на исходный список, а не копия:** > ```kotlin > 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` неоднозначен:** > ```kotlin > 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` работает, но создаёт новую коллекцию:** > ```kotlin > 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` оператор `+=` не скомпилируется. В цикле по большому списку `+=` на read-only коллекции даёт O(n²) копирований. Далее: [Изменяемость](Изменяемость.md)