Avatar ·

Как правильно скопировать список (list) в Kotlin: клонирование, копирование и глубокое копирование

Всем привет! Я только начал изучать Kotlin и столкнулся с неожиданной проблемой при работе со списками. У меня есть список чисел `originalList`, и я хочу сделать его копию, чтобы изменять копию, не затрагивая оригинал. Я попробовал просто присвоить один список другому: ```kotlin fun main() { val originalList = mutableListOf(1, 2, 3, 4, 5) val copiedList = originalList copiedList.add(6) println("Original: $originalList") // [1, 2, 3, 4, 5, 6] — изменился! println("Copied: $copiedList") // [1, 2, 3, 4, 5, 6] } ``` Как видите, при изменении `copiedList` меняется и `originalList`. Это происходит, потому что переменная `copiedList` ссылается на тот же объект в памяти, а не на новую копию данных. Я знаю, что в Java для копирования списков использовался `new ArrayList<>(originalList)`. Но в Kotlin есть более идиоматичные способы? Есть ли методы вроде `.clone()`, `.copy()`, или `toList()`? И что делать, если в списке лежат сложные объекты, которые тоже нужно скопировать (глубокое копирование)? Буду благодарен за примеры и объяснения! <<>>

Основная проблема: ссылочная привязка

В Kotlin, как и в Java, присваивание переменной списка другой переменной не создаёт новый список — обе переменные ссылаются на один и тот же объект в памяти. Поэтому любые изменения через одну ссылку видны через другую.

Стандартные способы копирования списка

Kotlin предоставляет несколько встроенных методов для создания поверхностной копии списка (shallow copy). Вот самые распространённые:

  • .toList() — создаёт новую неизменяемую копию списка (возвращает List)
  • .toMutableList() — создаёт новую изменяемую копию
  • ArrayList(originalList) — конструктор, как в Java
  • .map { it } — преобразование через map (тоже создаёт новую коллекцию)

Пример: поверхностное копирование

fun main() {
    val original = mutableListOf(1, 2, 3, 4, 5)

    // Вариант 1: toList() — создаёт неизменяемую копию
    val copy1: List<Int> = original.toList()

    // Вариант 2: toMutableList() — создаёт изменяемую копию
    val copy2: MutableList<Int> = original.toMutableList()

    // Вариант 3: через конструктор ArrayList
    val copy3 = ArrayList(original)

    // Вариант 4: через map
    val copy4 = original.map { it }

    copy2.add(6)
    println("Original: $original")  // [1, 2, 3, 4, 5] — не изменился
    println("Copy2: $copy2")        // [1, 2, 3, 4, 5, 6]
}

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

Глубокое копирование (deep copy)

Если ваш список содержит изменяемые объекты (например, data class с полями), и вы хотите, чтобы копия была полностью независимой, нужно выполнить глубокое копирование. Для этого:

  1. Используйте .map { it.copy() } для data class (у них есть встроенный метод .copy()).
  2. Для обычных классов — реализуйте собственный механизм копирования (например, через конструктор копирования или сериализацию).

Пример с data class:

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

fun main() {
    val people = listOf(
        Person("Alice", 25),
        Person("Bob", 30)
    )

    // Поверхностная копия — объекты общие
    val shallowCopy = people.toList()
    shallowCopy[0].age = 99
    println("Original: $people")  // [Person(name=Alice, age=99), ...] — изменился!

    // Глубокая копия — каждый объект скопирован
    val deepCopy = people.map { it.copy() }
    deepCopy[0].age = 10
    println("Original: $people")  // [Person(name=Alice, age=99), ...] — не изменился
    println("DeepCopy: $deepCopy") // [Person(name=Alice, age=10), ...]
}

Дополнительные советы

  • Для неизменяемых списков (listOf) копирование обычно не нужно — они и так не меняются.
  • Если вам нужна только неизменяемая копия, используйте .toList() — она возвращает List, но сам список неизменяемый.
  • Для больших списков с простыми типами данных .toMutableList() и ArrayList() работают одинаково эффективно.
  • Метод .clone() в Kotlin для списков не рекомендуется — он определён только для массивов (Array).

Итоговый вывод

Для копирования списка в Kotlin используйте .toList() или .toMutableList() — это идиоматичный и простой способ. Если внутри списка лежат изменяемые объекты, обязательно делайте глубокое копирование через map { it.copy() } (для data class) или аналогичный механизм.

<<>>

Альтернативный подход: использование расширений и фильтрации

Иногда вам нужно не просто скопировать список, но и отфильтровать или изменить элементы в процессе копирования. В Kotlin для этого удобно использовать функции filter, mapNotNull и другие.

Пример: копирование с фильтрацией

val numbers = listOf(1, 2, 3, 4, 5)

// Копируем только чётные числа
val evenNumbers = numbers.filter { it % 2 == 0 }.toList()
println(evenNumbers) // [2, 4]

Пример: копирование с преобразованием

val words = listOf("kotlin", "java", "python")

// Копируем с изменением регистра
val upperWords = words.map { it.uppercase() }.toList()
println(upperWords) // [KOTLIN, JAVA, PYTHON]

Использование Sequence для больших списков

Если вы работаете с очень большими списками (миллионы элементов), и копирование занимает много памяти, используйте Sequence:

val largeList = (1..10_000_000).toList()

// Ленивое копирование — элементы обрабатываются по мере необходимости
val copySequence = largeList.asSequence().toList()

Однако помните: Sequence не ускоряет само копирование, но может быть полезен, если вы сразу применяете несколько операций (filter, map) — они будут выполнены за один проход.

Когда использовать эти подходы?

  • Если нужно просто скопировать список — используйте toList() или toMutableList().
  • Если нужно скопировать и сразу модифицировать — комбинируйте с map, filter и т.д.
  • Если нужна максимальная производительность при множественных операциях — рассмотрите Sequence.