Клонируем списки в Kotlin: глубокое vs поверхностное копирование

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Клонирование списков в 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(), иначе получите ошибку ком

Блоги

Рекомендаций по книгам