Files
obsidian/Kotlin/Коллекции/Изменяемость.md
T

324 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: Изменяемость
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)