Files
obsidian/Kotlin/Коллекции/List, Set, Map.md
T

366 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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<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`:
```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{"Дубликаты важны<br/>или нужен индекс?"}
C -->|да| L["List"]
C -->|нет| D{"Частый поиск<br/>элемента?"}
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<K,V> 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<Int>` неоднозначен:**
> ```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<Int>` оператор `+=` не скомпилируется. В цикле по большому списку `+=` на read-only коллекции даёт O(n²) копирований.
Далее: [Изменяемость](Изменяемость.md)