Avatar ·

Как скопировать или клонировать список в Kotlin: полное руководство

Привет! Я только начал изучать Kotlin после Java, и у меня возникла проблема с копированием списков. В Java я привык делать что-то вроде `new ArrayList<>(originalList)`, чтобы создать независимую копию. Но в Kotlin я заметил, что простое присваивание `val copy = originalList` не создаёт копию, а лишь ссылку на тот же объект. Я попробовал `originalList.toList()`, но когда я меняю элементы в копии, изменения всё равно отражаются в оригинале? Или я что-то путаю? Вот пример, который меня смущает: ```kotlin data class Person(val name: String, var age: Int) fun main() { val original = mutableListOf(Person("Alice", 25), Person("Bob", 30)) val copy = original.toList() // это же копия? // Пробую изменить (copy as MutableList)[0].age = 26 // Изменяю возраст в "копии" println(original[0].age) // Выводит 26! Почему оригинал тоже изменился? } ``` Почему `toList()` не создаёт полную независимую копию? Как правильно сделать глубокое копирование списка объектов? Есть ли встроенные функции для этого? Буду благодарен за подробное объяснение с примерами. <<>>

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

То, с чем вы столкнулись — классическая проблема поверхностного копирования (shallow copy). toList() создаёт новый список, но копирует только ссылки на объекты, а не сами объекты. Поэтому оба списка указывают на одни и те же экземпляры Person.

Способы копирования списков в Kotlin

1. Поверхностное копирование (shallow copy)

Создаёт новый список с теми же элементами (ссылками):

val original = listOf(1, 2, 3)
val copy1 = original.toList()          // Для неизменяемых списков
val copy2 = original.toMutableList()   // Для изменяемых списков

// Или через конструктор:
val copy3 = ArrayList(original)
val copy4 = mutableListOf<Int>().apply { addAll(original) }

2. Полное глубокое копирование (deep copy)

Создаёт новый список, где каждый элемент — тоже новый объект. Для этого нужно вручную копировать каждый элемент:

data class Person(val name: String, var age: Int) {
    // Функция копирования для глубокого клонирования
    fun deepCopy() = Person(name, age)
}

fun main() {
    val original = mutableListOf(Person("Alice", 25), Person("Bob", 30))
    
    // Глубокое копирование через map
    val deepCopy = original.map { it.deepCopy() }.toMutableList()
    
    // Теперь изменения в копии не влияют на оригинал
    deepCopy[0].age = 26
    println(original[0].age) // 25 ✅
    println(deepCopy[0].age) // 26 ✅
}

Стандартные методы для поверхностного копирования

  • toList() — возвращает неизменяемый список
  • toMutableList() — возвращает изменяемый список
  • toCollection() — копирует в указанную коллекцию
  • Оператор +: val copy = listOf() + original

Особые случаи

Для примитивных типов (Int, String, Boolean): поверхностное копирование работает как глубокое, т.к. они иммутабельны.

val numbers = listOf(1, 2, 3)
val copy = numbers.toMutableList()
copy[0] = 100
println(numbers[0]) // 1 — безопасно

Для вложенных списков: нужно рекурсивное копирование:

val nested = listOf(listOf(1, 2), listOf(3, 4))
val deepCopyNested = nested.map { it.toList() }

Рекомендации

  • Используйте toMutableList() для быстрого поверхностного копирования
  • Для глубокого копирования всегда пишите явную логику через map или copy() для data-классов
  • Если данные сложные (с циклическими ссылками), рассмотрите сериализацию (JSON/Protobuf)
// Пример с data class copy()
data class Address(val city: String, val street: String)
data class User(val name: String, val address: Address) {
    fun deepCopy() = User(name, address.copy())
}
<<>>

Альтернативный подход: использование библиотек для глубокого копирования

Если у вас сложная иерархия объектов, ручное копирование может быть утомительным. В таких случаях можно использовать сериализацию:

import kotlinx.serialization.*
import kotlinx.serialization.json.*

@Serializable
data class Person(val name: String, var age: Int)

fun <T> deepCopyViaJson(obj: T): T {
    val json = Json { ignoreUnknownKeys = true }
    val string = json.encodeToString(obj)
    return json.decodeFromString(string)
}

fun main() {
    val original = listOf(Person("Alice", 25), Person("Bob", 30))
    val copy = original.map { deepCopyViaJson(it) }
    
    copy[0].age = 26
    println(original[0].age) // 25
}

Плюсы: автоматически обрабатывает вложенные объекты, не требует написания кода копирования для каждого класса.

Минусы: медленнее, требует аннотаций @Serializable, не работает с циклическими ссылками.

Также можно использовать библиотеку kopy (Kotlin Copy Builder) для генерации функций глубокого копирования через аннотации.

Для простых случаев рекомендую придерживаться ручного подхода из первого ответа — он прозрачнее и быстрее.