--- 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 = 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 = 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 = 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` даёт лишь половину гарантии. ## 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() val tracks: List 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 = persistentListOf(1, 2, 3) // val updated = list.add(4) // новый список, старый цел // println("$list / $updated") // [1, 2, 3] / [1, 2, 3, 4] // // Тип ImmutableList гарантирует, что коллекция не изменится вообще, // а PersistentList добавляет эффективное «обновление» без полного копирования. ``` Отличие от `toList()`: персистентная коллекция при добавлении переиспользует внутреннюю структуру, поэтому `add` стоит O(log n), а не O(n). Это имеет смысл, когда обновления частые, а коллекция большая. > [!info] Отличия от Java > | Java | Kotlin | > |---|---| > | `Collections.unmodifiableList(x)` | тип `List` | > | `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 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 = items > } > > fun main() { > val l = Leaky2() > val view = l.items() > @Suppress("UNCHECKED_CAST") > (view as MutableList).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()` изменяется свободно. > > **Из Java read-only коллекция изменяема:** > ```kotlin > fun provide(): List = 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` в цикле - это O(n²):** > ```kotlin > fun main() { > var slow = listOf() > 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)