Kotlin data class copy method not deep copying all members

KotlinCopyDeep CopyData Class

Kotlin Problem Overview


Could someone explain how exactly the copy method for Kotlin data classes work? It seems like for some members, a (deep) copy is not actually created and the references are still to the original.

fun test() {
    val bar = Bar(0)
    val foo = Foo(5, bar, mutableListOf(1, 2, 3))
    println("foo    : $foo")

    val barCopy = bar.copy()
    val fooCopy = foo.copy()
    foo.a = 10
    bar.x = 2
    foo.list.add(4)

    println("foo    : $foo")
    println("fooCopy: $fooCopy")
    println("barCopy: $barCopy")
}

data class Foo(var a: Int,
               val bar: Bar,
               val list: MutableList<Int> = mutableListOf())

data class Bar(var x: Int = 0)

>Output:
>foo : Foo(a=5, bar=Bar(x=0), list=[1, 2, 3])
>foo : Foo(a=10, bar=Bar(x=2), list=[1, 2, 3, 4])
>fooCopy: Foo(a=5, bar=Bar(x=2), list=[1, 2, 3, 4])
>barCopy: Bar(x=0)

Why is barCopy.x=0 (expected), but fooCopy.bar.x=2 (I would think it would be 0). Since Bar is also a data class, I would expect foo.bar to also be a copy when foo.copy() is executed.

To deep copy all members, I can do something like this:

val fooCopy = foo.copy(bar = foo.bar.copy(), list = foo.list.toMutableList())

> fooCopy: Foo(a=5, bar=Bar(x=0), list=[1, 2, 3])

But am I missing something or is there a better way to do this without needing to specify that these members need to force a deep copy?

Kotlin Solutions


Solution 1 - Kotlin

The copy method of Kotlin is not supposed to be a deep copy at all. As explained in the reference doc (https://kotlinlang.org/docs/reference/data-classes.html), for a class such as:

data class User(val name: String = "", val age: Int = 0)

the copy implementation would be:

fun copy(name: String = this.name, age: Int = this.age) = User(name, age)

So as you can see, it's a shallow copy. The implementations of copy in your specific cases would be:

fun copy(a: Int = this.a, bar: Bar = this.bar, list: MutableList<Int> = this.list) = Foo(a, bar, list)

fun copy(x: Int = this.x) = Bar(x)

Solution 2 - Kotlin

There is a way to make a deep copy of an object in Kotlin (and Java): serialize it to memory and then deserialize it back to a new object. This will only work if all the data contained in the object are either primitives or implement the Serializable interface

Here is an explanation with sample Kotlin code https://rosettacode.org/wiki/Deepcopy#Kotlin

import java.io.Serializable
import java.io.ByteArrayOutputStream
import java.io.ByteArrayInputStream
import java.io.ObjectOutputStream
import java.io.ObjectInputStream
 
fun <T : Serializable> deepCopy(obj: T?): T? {
    if (obj == null) return null
    val baos = ByteArrayOutputStream()
    val oos  = ObjectOutputStream(baos)
    oos.writeObject(obj)
    oos.close()
    val bais = ByteArrayInputStream(baos.toByteArray())
    val ois  = ObjectInputStream(bais)
    @Suppress("unchecked_cast")
    return ois.readObject() as T
} 

Note: This solution should also be applicable in Android using the Parcelable interface instead of the Serializable. Parcelable is more efficient.

Solution 3 - Kotlin

As @Ekeko said, the default copy() function implemented for data class is a shallow copy which looks like this:

fun copy(a: Int = this.a, bar: Bar = this.bar, list: MutableList<Int> = this.list)

To perform a deep copy, you have to override the copy() function.

fun copy(a: Int = this.a, bar: Bar = this.bar.copy(), list: MutableList<Int> = this.list.toList()) = Foo(a, bar, list)

Solution 4 - Kotlin

Beware of those answers who are just copying list reference from an old object into the new one. One quick way (not very efficient, though) of deep copying is to serialize/deserialize objects i.e. convert the objects into JSON and then transform them back to POJO. If you are using GSON, here is a quick piece of code:

class Foo {
    fun deepCopy() : Foo {
        return Gson().fromJson(Gson().toJson(this), this.javaClass)
    }
}

Solution 5 - Kotlin

Building on a previous answer, an easy if somewhat inelegant solution is to use the kotlinx.serialization facility. Add the plugin to build.gradle as per the docs, then to make a deep copy of an object, annotate it with @Serializable and add a copy method which converts the object to a serialised binary form, then back again. The new object will not reference any objects in the original.

import kotlinx.serialization.Serializable
import kotlinx.serialization.cbor.Cbor

@Serializable
data class DataClass(val yourData: Whatever, val yourList: List<Stuff>) {

    var moreStuff: Map<String, String> = mapOf()

    fun copy(): DataClass {
        return Cbor.load(serializer(), Cbor.dump(serializer(), this))
    }

This won't be as fast as a handwritten copy function, but it does not require updating if the object is changed, so is more robust.

Solution 6 - Kotlin

I face the same problem. Because in kotlin, ArrayList.map {it.copy} not copying all items of an object specially if a member is list of another object inside this.

The only solution, for deep copying of all items of an object I found on the web, is to serialize and deserialize the object when you send or assign it to a new variable. Code like as follows.

@Parcelize
data class Flights(

// data with different types including the list 
    
) : Parcelable

Before I receiving List of Flights, We can use JSON to deserialize the Object and serialize the object same time!!!.

First, we create two extension functions.

// deserialize method
fun flightListToString(list: ArrayList<Flights>): String {
    val type = object : TypeToken<ArrayList<Flights>>() {}.type
    return Gson().toJson(list, type)
}

// serialize method
fun toFlightList(string: String): List<Flights>? {
    val itemType = object : TypeToken<ArrayList<Flights>>() {}.type
    return Gson().fromJson<ArrayList<Flights>>(string, itemType)
}

We can use it like below.

   // here I assign list from Navigation args

    private lateinit var originalFlightList: List<Flights>
    ...
    val temporaryList = ArrayList(makeProposalFragmentArgs.selectedFlightList.asList())    
    originalFlightList = toFlightList(flightListToString(temporaryList))!! 

Later, I send this list to Recycler Adapter & there the content of the Flights object would be modified.

bindingView.imageViewReset.setOnClickListener {
        val temporaryList = ArrayList(makeProposalFragmentArgs.selectedFlightList.asList())
        val flightList = toFlightList(flightListToString(temporaryList))!!
        **adapter**.resetListToOriginal(flightList)
    }

Solution 7 - Kotlin

Maybe you can use kotlin reflection in some way here, this example is not recursive but should give the idea:

fun DataType.deepCopy() : DataType {
    val copy = DataType()

    for (m in this::class.members) {
        if (m is KProperty && m is KMutableProperty) {
            m.setter.call(copy, if (m.returnType::class.isData) {
                (m.getter.call(this) to m.returnType).copy()
            } else m.setter.call(copy, m.getter.call(this)))
        }
    }

    return copy
}

Solution 8 - Kotlin

If you use Jackson and not concerned about performance,then this simple extension function will give you this feature.

private val objectMapper = ObjectMapper()
.registerModule(KotlinModule())
.registerModule(JavaTimeModule())
    
fun <T> Any.copyDeep(): T {
        return objectMapper.readValue(objectMapper.writeValueAsString(this), this.javaClass) as T
    }

Solution 9 - Kotlin

Use this function:

private val gson = Gson()

fun <T> deepCopy(item: T?, clazz: Class<T>): T {
        val str = gson.toJson(item)
        return gson.fromJson(str, clazz)
    }

Solution 10 - Kotlin

What you want is a deep copy. There are many tools available to do this.

  1. MapStruct: https://mapstruct.org/

Mapstruct generates code at compile time. Normally, it is to auto-generate mappers between java objects, but it also has a 'clone' functionality to create a deep copy of an object. Since this is generated code of what you'd manually write, it is the fastest way to achieve this.

There are many more (kryo, dozer, etc...), you can actually just google, for example here: https://programmer.group/performance-comparison-between-shallow-and-deep-copies.html

DO AVOID serialization-based 'clone': apache commons' SerializationUtils, jackson, gson, etc... They have a huge overhead, since it first creates a middle state. They are about 10-100 times slower than actual copying.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestiontriadView Question on Stackoverflow
Solution 1 - KotlinEkekoView Answer on Stackoverflow
Solution 2 - KotlinSebas LGView Answer on Stackoverflow
Solution 3 - KotlinCrazyAppleView Answer on Stackoverflow
Solution 4 - KotlinMuhammad MuzammilView Answer on Stackoverflow
Solution 5 - KotlinClydeView Answer on Stackoverflow
Solution 6 - KotlinShihab UddinView Answer on Stackoverflow
Solution 7 - KotlinTreviñoView Answer on Stackoverflow
Solution 8 - KotlinMannieView Answer on Stackoverflow
Solution 9 - KotlintohidmahmoudvandView Answer on Stackoverflow
Solution 10 - KotlinAgoston HorvathView Answer on Stackoverflow