324 lines
16 KiB
Markdown
324 lines
16 KiB
Markdown
---
|
||
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)
|