Kotlin guide done. (20.10.26)
This commit is contained in:
@@ -0,0 +1,365 @@
|
||||
---
|
||||
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)
|
||||
Reference in New Issue
Block a user