Клонирование списков в Kotlin: как не наломать дров с ссылками?
Представьте: вы пишете приложение на Kotlin, обрабатываете список данных, и внезапно исходный массив меняется сам по себе. Знакомая боль? Скорее всего, вы просто скопировали ссылку, а не сам список. В этой статье мы разберем все легальные способы клонировать список в Kotlin — от поверхностного копирования до глубокого. Без воды, только код и пояснения.
Тема навеяна популярным вопросом на Stack Overflow (более 200 000 просмотров): "How to clone or copy a list in kotlin". Давайте раз и навсегда разберемся, как правильно скопировать список Kotlin, чтобы не потерять данные и не сойти с ума.
Почему простое присваивание не работает?
В Kotlin, как и в Java, списки — это ссылочные типы. Если вы напишете:
val original = mutableListOf("A", "B", "C")
val copy = original // Это НЕ копия!
copy.add("D")
println(original) // [A, B, C, D] — исходник изменился!
Вы просто создали вторую ссылку на тот же объект в памяти. Любое изменение через copy отразится на original. Чтобы этого избежать, нужно использовать специальные методы копирования списка Kotlin.
Топ-5 способов скопировать список в Kotlin
Мы рассмотрим все основные варианты: от простого toList() до ручного глубокого копирования. Выбирайте подходящий под вашу задачу.
1. toList() — самый простой способ
Метод toList() создаёт новую неизменяемую копию списка. Это поверхностное копирование (shallow copy) — сами элементы не дублируются, но структура списка становится независимой.
val original = listOf("Kotlin", "Java", "Python")
val copy = original.toList()
// copy — это новый объект
println(copy === original) // false — разные ссылки
println(copy == original) // true — одинаковое содержимое
Идеально для простых списков строк или чисел. Если вам нужна изменяемая копия — используйте toMutableList().
2. toMutableList() — для изменяемых копий
Хотите копию, которую можно менять? toMutableList() — ваш выбор. Он создаёт новый MutableList с теми же элементами.
val original = listOf(1, 2, 3)
val mutableCopy = original.toMutableList()
mutableCopy.add(4)
println(original) // [1, 2, 3]
println(mutableCopy) // [1, 2, 3, 4] — исходник не тронут
Это самый популярный способ клонировать mutable list в Kotlin.
3. ArrayList(collection) — классика Java
Если вы пришли из Java, этот способ покажется родным. Конструктор ArrayList принимает другую коллекцию и копирует её элементы.
val original = listOf("A", "B", "C")
val arrayCopy = ArrayList(original)
arrayCopy[0] = "Z"
println(original) // [A, B, C]
println(arrayCopy) // [Z, B, C]
Работает быстро и предсказуемо. Но помните: это тоже shallow copy.
4. map() — с трансформацией элементов
Если вам нужно не просто скопировать, но и преобразовать элементы, используйте map(). Он создаёт новый список на основе старого, применяя к каждому элементу лямбду.
val numbers = listOf(10, 20, 30)
val doubled = numbers.map { it * 2 }
println(numbers) // [10, 20, 30]
println(doubled) // [20, 40, 60]
Для простого копирования без изменений можно написать numbers.map { it }, но это избыточно — лучше используйте toList().
5. Глубокое копирование (deep copy) для сложных объектов
Все предыдущие методы делают поверхностное копирование. Если ваш список содержит изменяемые объекты (например, data class с полями), то изменение объекта в копии затронет оригинал:
data class User(val name: String, var age: Int)
val original = listOf(User("Alice", 25), User("Bob", 30))
val copy = original.toMutableList()
copy[0].age = 99 // Меняем возраст в копии
println(original[0].age) // 99 — оригинал тоже изменился!
Чтобы избежать этого, нужно делать глубокое копирование (deep copy). Варианты:
- Вручную: создать новые объекты через конструктор
- Через copy() для data class (если поля примитивные или неизменяемые)
- Сериализация/десериализация (например, через Gson или kotlinx.serialization)
Пример с ручным deep copy:
data class User(val name: String, var age: Int)
val original = listOf(User("Alice", 25), User("Bob", 30))
val deepCopy = original.map { it.copy() } // copy() создаёт новый объект
deepCopy[0].age = 99
println(original[0].age) // 25 — оригинал не изменился!
Если объекты содержат вложенные списки или другие сложные структуры, придётся реализовать глубокое копирование рекурсивно или через сериализацию.
Сравнение методов копирования списков в Kotlin
Чтобы вы могли быстро выбрать, вот краткая таблица:
- toList() — неизменяемая копия, shallow copy
- toMutableList() — изменяемая копия, shallow copy
- ArrayList(collection) — изменяемая копия, shallow copy
- map { it } — функциональный стиль, shallow copy
- map { it.copy() } — deep copy для data class
- Сериализация — полный deep copy для любых объектов
Частые ошибки при клонировании списков
Даже опытные разработчики иногда спотыкаются. Вот топ-3 грабли:
Ошибка 1: Присваивание вместо копирования
Мы уже разобрали — val copy = original не создаёт новый список.
Ошибка 2: Забыли про shallow copy
Используете toList() и думаете, что элементы защищены? Нет, если это mutable объекты — они остаются общими.
Ошибка 3: Путаница между List и MutableList
Если вам нужно изменять копию, не забудьте вызвать toMutableList(), иначе получите ошибку ком