Как скопировать или клонировать список в Kotlin: полное руководство
Проблема: поверхностное 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) для генерации функций глубокого копирования через аннотации.
Для простых случаев рекомендую придерживаться ручного подхода из первого ответа — он прозрачнее и быстрее.