Kotlin guide done. (20.10.26)

This commit is contained in:
2026-10-20 12:54:25 +03:00
parent da3ad9dd1a
commit 6853ada348
81 changed files with 30112 additions and 0 deletions
@@ -0,0 +1,323 @@
---
title: Изменяемость
description: Read-only против immutable, защитное копирование и персистентные коллекции
tags:
- kotlin
- collections
---
`List` в Kotlin - **read-only**, а не immutable. Это разные вещи, и их смешение - источник самых неприятных ошибок с коллекциями.
| | read-only (`List`) | immutable |
|---|---|---|
| изменить через эту ссылку | нельзя | нельзя |
| может измениться вообще | **да** | нет |
| гарантия | интерфейс без мутаторов | сам объект неизменяем |
| есть в Kotlin stdlib | да | нет (только внешняя библиотека) |
## Почему read-only не immutable
```kotlin
fun main() {
val mutable = mutableListOf(1, 2)
val readOnly: List<Int> = mutable // тот же объект, другой интерфейс
mutable.add(3)
println(readOnly) // [1, 2, 3] — изменился под вами
}
```
`MutableList` наследует `List`, поэтому любая изменяемая коллекция подходит там, где ожидается `List`. Приведение типа объект не копирует.
Это создаёт конкретную проблему в API:
```kotlin
class Repository4 {
private val items = mutableListOf(1, 2, 3)
fun getItems(): List<Int> = items // утечка: вернули ссылку на изменяемое
fun add(x: Int) { items.add(x) }
}
fun main() {
val repo = Repository4()
val snapshot = repo.getItems()
println(snapshot) // [1, 2, 3]
repo.add(4)
println(snapshot) // [1, 2, 3, 4] — «снимок» изменился
}
```
Клиент получил `List` и вправе считать, что значение стабильно - например, положить его в `Map` как ключ или обойти в другом потоке. Ни то ни другое здесь не безопасно.
## Защитное копирование
```kotlin
class Repository5 {
private val items = mutableListOf(1, 2, 3)
fun getItems(): List<Int> = items.toList() // копия
fun add(x: Int) { items.add(x) }
}
fun main() {
val repo = Repository5()
val snapshot = repo.getItems()
repo.add(4)
println("$snapshot / ${repo.getItems()}") // [1, 2, 3] / [1, 2, 3, 4]
}
```
`toList()` создаёт новый `ArrayList`. Цена - O(n) и лишний объект; выгода - значение, на которое можно полагаться.
Варианты копирования:
```kotlin
fun main() {
val source = mutableListOf(1, 2, 3)
println(source.toList()::class.simpleName) // ArrayList
println(source.toMutableList()::class.simpleName) // ArrayList
println(source.toSet()::class.simpleName) // LinkedHashSet
println(ArrayList(source)::class.simpleName) // ArrayList
println(List(source.size) { source[it] }) // [1, 2, 3]
}
```
Когда копировать:
- коллекция уходит за границу класса или модуля и должна быть стабильной
- коллекция попадает в `Map`/`Set` как ключ или элемент
- значение читается из другого потока
Когда не копировать:
- коллекция создана тут же и больше нигде не хранится
- вызывающий - код того же класса
- коллекция огромная, а изменения точно не будет (документируйте это)
## Неизменяемость на уровне содержимого
Даже честная копия списка не защищает от изменения **элементов**:
```kotlin
class Item(var name: String)
fun main() {
val items = listOf(Item("a"), Item("b"))
val copy = items.toList()
items[0].name = "изменено"
println(copy[0].name) // изменено — копия поверхностная
}
```
`toList()` копирует только ссылки. Настоящая защита - неизменяемые элементы:
```kotlin
data class ItemRo(val name: String)
fun main() {
val items = listOf(ItemRo("a"))
// items[0].name = "x" // ошибка: val
val changed = items.map { it.copy(name = "изменено") }
println("${items[0].name} / ${changed[0].name}") // a / изменено
}
```
Это общее правило: неизменяемость полезна, когда она рекурсивна. `List<T>` с изменяемым `T` даёт лишь половину гарантии.
## val и изменяемость
Два независимых понятия, которые постоянно путают:
```kotlin
fun main() {
val a = mutableListOf(1) // ссылку сменить нельзя, содержимое — можно
a.add(2)
println(a) // [1, 2]
// a = mutableListOf(3) // ошибка
var b = listOf(1) // ссылку сменить можно, содержимое — нельзя
b = listOf(1, 2)
println(b) // [1, 2]
// b.add(3) // ошибка
}
```
| | `val` | `var` |
|---|---|---|
| `List` | ничего не изменить | можно заменить целиком |
| `MutableList` | можно менять содержимое | и то и другое |
Самый частый правильный выбор - `val` + `List`. Подробнее: [Переменные](../Синтаксис%20и%20типы/Базовый%20синтаксис/Переменные.md)
## Функциональное обновление
Вместо изменения на месте - новое значение:
```kotlin
fun main() {
val original = listOf(1, 2, 3)
println(original + 4) // [1, 2, 3, 4]
println(original - 2) // [1, 3]
println(original.map { it * 10 }) // [10, 20, 30]
println(original.filter { it != 2 }) // [1, 3]
println(original.toMutableList().apply { this[0] = 99 }) // [99, 2, 3]
println(original) // [1, 2, 3] — не тронут
}
```
Для `Map` то же:
```kotlin
fun main() {
val config = mapOf("host" to "localhost", "port" to "8080")
println(config + ("port" to "9090")) // {host=localhost, port=9090}
println(config - "port") // {host=localhost}
println(config + mapOf("tls" to "on")) // {host=localhost, port=8080, tls=on}
}
```
Цена - копирование на каждую операцию. Для небольших коллекций и нечастых обновлений это правильный выбор по умолчанию: такой код проще отлаживать, потому что значение не меняется за спиной.
## Изменяемое состояние внутри, read-only снаружи
Рабочий шаблон для класса с коллекцией:
```kotlin
class Playlist {
private val _tracks = mutableListOf<String>()
val tracks: List<String> get() = _tracks.toList() // копия при чтении
fun add(track: String) { _tracks.add(track) }
fun size() = _tracks.size
}
fun main() {
val p = Playlist()
p.add("песня 1")
val snapshot = p.tracks
p.add("песня 2")
println("$snapshot / ${p.tracks}") // [песня 1] / [песня 1, песня 2]
}
```
Если копия на каждое чтение слишком дорога, альтернатива - возвращать `_tracks` напрямую и документировать, что значение живое. Тогда тип должен быть осмысленным: `List` здесь обманывает читателя, и честнее отдавать `Sequence` или явный метод `snapshot()`.
## Неизменяемость и многопоточность
```kotlin
fun main() {
val shared = mutableListOf(1, 2, 3)
// Небезопасно: обход в одном потоке + add в другом = ConcurrentModificationException
// Безопасно: работать с копией
val safe = shared.toList()
println(safe.sum()) // 6
val sync = java.util.Collections.synchronizedList(mutableListOf(1, 2))
println(sync.size) // 2 — но обход всё равно нужно синхронизировать вручную
val concurrent = java.util.concurrent.CopyOnWriteArrayList(listOf(1, 2))
println(concurrent.size) // 2
}
```
Неизменяемое значение потокобезопасно по определению - именно поэтому в конкурентном коде предпочитают read-only копии, а не блокировки. Подробнее: [Разделяемое состояние](../Корутины/Разделяемое%20состояние.md)
## Персистентные коллекции
В stdlib настоящих immutable-коллекций нет. Их даёт библиотека `kotlinx.collections.immutable`:
```kotlin
// Требует зависимости org.jetbrains.kotlinx:kotlinx-collections-immutable.
// val list: PersistentList<Int> = persistentListOf(1, 2, 3)
// val updated = list.add(4) // новый список, старый цел
// println("$list / $updated") // [1, 2, 3] / [1, 2, 3, 4]
//
// Тип ImmutableList<T> гарантирует, что коллекция не изменится вообще,
// а PersistentList<T> добавляет эффективное «обновление» без полного копирования.
```
Отличие от `toList()`: персистентная коллекция при добавлении переиспользует внутреннюю структуру, поэтому `add` стоит O(log n), а не O(n). Это имеет смысл, когда обновления частые, а коллекция большая.
> [!info] Отличия от Java
> | Java | Kotlin |
> |---|---|
> | `Collections.unmodifiableList(x)` | тип `List<T>` |
> | `List.copyOf(x)` (Java 10+) | `x.toList()` |
> | `List.of(1, 2)` - immutable | `listOf(1, 2)` - read-only |
> | нарушение видно в рантайме | видно при компиляции |
> | `final List` | `val list` |
> | Guava `ImmutableList` | `kotlinx.collections.immutable` |
>
> **`unmodifiableList` и `List` решают одну задачу по-разному.** Java-обёртка - объект-декоратор: тип остаётся `List`, метод `add` есть, но бросает исключение в рантайме. Kotlin убирает метод из интерфейса, поэтому ошибка видна компилятору и обёртка не нужна:
> ```java
> List<Integer> ro = Collections.unmodifiableList(src);
> ro.add(1); // компилируется, падает в рантайме
> ```
> Но у Java-обёртки есть преимущество, которого нет у Kotlin: она действительно запрещает изменение через **эту** ссылку даже при касте. Kotlin-ский `List` каст к `MutableList` может пережить, если под ним `ArrayList`.
>
> **`List.of` в Java сильнее, чем `listOf` в Kotlin.** `List.of(1, 2)` создаёт по-настоящему неизменяемую коллекцию: любая попытка изменения, в том числе из другого места, бросит исключение. `listOf(1, 2)` даёт неизменяемую реализацию только случайно - это деталь, на которую нельзя полагаться, потому что `listOf` на самом деле описывает интерфейс, а не гарантию объекта.
>
> **Защитное копирование нужно в обоих языках.** `private final List` в Java и `private val` в Kotlin одинаково не защищают содержимое. Разница в том, что в Kotlin ошибку легче не заметить: тип `List` в сигнатуре выглядит как обещание неизменяемости, которого он не даёт.
>
> Сравнить: [Инкапсуляция в Java](../../Программирование%20на%20Java/ООП/Инкапсуляция/Инкапсуляция.md)
> [!warning] Подводные камни
> **Возврат `MutableList` под типом `List` - утечка:**
> ```kotlin
> class Leaky2 {
> private val items = mutableListOf(1, 2)
> fun items(): List<Int> = items
> }
>
> fun main() {
> val l = Leaky2()
> val view = l.items()
> @Suppress("UNCHECKED_CAST")
> (view as MutableList<Int>).add(3) // клиент изменил приватное состояние
> println(l.items()) // [1, 2, 3]
> }
> ```
> Каст сработал, потому что под ссылкой настоящий `ArrayList`. Защита - `toList()`.
>
> **`toList()` копирует поверхностно** - элементы остаются общими. Для вложенных структур нужна глубокая копия:
> ```kotlin
> fun main() {
> val nested = listOf(mutableListOf(1), mutableListOf(2))
> val shallow = nested.toList()
> nested[0].add(99)
> println(shallow[0]) // [1, 99] — внутренний список общий
>
> val deep = nested.map { it.toList() }
> nested[0].add(100)
> println(deep[0]) // [1, 99] — а этот цел
> }
> ```
>
> **`val` не делает коллекцию неизменяемой** - только ссылку. `val items = mutableListOf<Int>()` изменяется свободно.
>
> **Из Java read-only коллекция изменяема:**
> ```kotlin
> fun provide(): List<Int> = mutableListOf(1, 2)
>
> fun main() { println(provide()) } // [1, 2]
> ```
> Java-код получит `java.util.List` и вызовет `add` без ошибок компиляции. Для API, доступного из Java, копируйте или используйте `Collections.unmodifiableList`. Подробнее: [Kotlin из Java](../Взаимодействие%20с%20Java/Kotlin%20из%20Java.md)
>
> **Изменяемый объект как ключ `Map` теряется:**
> ```kotlin
> data class MutableKey(var id: Int)
>
> fun main() {
> val key = MutableKey(1)
> val map = mutableMapOf(key to "значение")
> key.id = 2
> println(map[key]) // null — хеш изменился
> println(map.size) // 1 — запись на месте, но недостижима
> }
> ```
>
> **`+=` на `var list: List<T>` в цикле - это O(n²):**
> ```kotlin
> fun main() {
> var slow = listOf<Int>()
> repeat(1000) { slow += it } // 1000 копирований списка
> println(slow.size) // 1000
>
> val fast = buildList { repeat(1000) { add(it) } }
> println(fast.size) // 1000, одна коллекция
> }
> ```
>
> **`ConcurrentModificationException` при изменении во время обхода:**
> ```kotlin
> fun main() {
> val list = mutableListOf(1, 2, 3)
> try {
> for (x in list) if (x == 2) list.remove(x)
> } catch (e: ConcurrentModificationException) {
> println("нельзя изменять во время обхода")
> }
> list.removeAll { it == 2 } // так правильно
> println(list) // [1, 3]
> }
> ```
>
> **Обёртка `synchronizedList` не делает обход потокобезопасным** - атомарны только отдельные методы. Итерацию нужно оборачивать в `synchronized` вручную.
Далее: [Операции](Операции.md)