85

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?

12 Answers 12

78

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)
Sign up to request clarification or add additional context in comments.

6 Comments

Be careful about copying lists this way. Your copy will be the same memory location, so changing one will affect both. A true deep copy will also copy the list items. See stackoverflow.com/q/51480079/1015595.
This shouldn't be the accepted answer. Copying like this is still just copying the reference of the list. Check my answer for detail
@MuhammadMuzammil The question is not about how to implement a proper deep copy but "Could someone explain how exactly the copy method for Kotlin data classes work?"
@Ekeko I beg to differ. This is about a proper implementation of deep copy. See this "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?"
@Ekeko I wrote an unit test to verify .copy() behaviour on data class and copied subject is a deep copy. pastes.io/bdmzouljgz Could you please explain why do you insist it is a shallow copy?
|
15

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)
    }
}

14 Comments

> either serialize/deserialize objects or to convert [...] into JSON so, you are implying like converting to/from JSON is something other than the serialization/deserialization itself.
No, what I'm implying is that converting into JSON is a specific, and in some cases easiest, form of serializing.
the best solution I found... Kudos @MuhammadMuzammil
This is TERRIBLE, please don't do this. This is about a 1000 times slower and uses 1000 times more memory than a simple deepcopy function from e.g. apache commons.
@MuhammadMuzammil That's a slippery word when unqualified. The proposed solution is quick (i.e., easy) to implement but probably not quick to run.
|
13

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)

1 Comment

With help of kotlin extensions functions it is better to extend class with a new method instead of overriding existing one -- let's leave default behaviour as is without bringing more complexity by mixing shallow and deep copies under the same name.
12

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.

3 Comments

Very nice.I think, it can be more useful with an extension like that : fun <T:Serializable?>T.deepCopy(): T? { if (this == null) return null val baos = ByteArrayOutputStream() val oos = ObjectOutputStream(baos) oos.writeObject(this) oos.close() val bais = ByteArrayInputStream(baos.toByteArray()) val ois = ObjectInputStream(bais) @Suppress("unchecked_cast") return ois.readObject() as T }
There must be something wrong with this implementation, why else Java/Kotlin team wouldn't add the same functionality? Or I'm missing some important info on this.
@Farid performance, this is not the most performant way to create a deep-copy.
1

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.

Comments

0

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
}

Comments

0

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)
    }

3 Comments

The same issue with this code as described in comments stackoverflow.com/a/58850583/3649629
@Gleichmut did you try with my solution
Shihab, I didn't launch this code. I came to this thread by researching nuances of shallow & deep copy in Kotlin. I mentioned my view on this in comments to answer post by link above, but I will repeat it here: this code would not pass code review in any team I ever worked so far. It is the bad practice and should not be use unless your a really deliver something quick&dirty.
0

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.

Comments

0

In case you have body parameters they also won't be copied. Many programmers keep there methods like this:

data class Data(
    val id: Int,
    val images: List<Image> = emptyList()
)  {
    // Body parameters
    var onImageClick: () -> Unit = { }
    var onLikeClick: () -> Unit = { }
}

When you copy this object, you get a new object with empty (default) body parameters (onImageClick and onLikeClick). To avoid this situation, just add a new method. Note that I use apply:

data class Data(
    val id: Int,
    val images: List<Image> = emptyList()
)  {
    var onImageClick: () -> Unit = { }
    var onLikeClick: () -> Unit = { }

    fun deepCopy(
        id: Int = this.id,
        images: List<Image> = this.images,
        onImageClickAction: () -> Unit = this.onImageClick,
        onLikeClickAction: () -> Unit = this.onLikeClick
    ) = Data(id = id,
            // Use deep copy of the list from above answers instead
            images = images).apply {
        onImageClick = onImageClickAction
        onLikeClick = onLikeClickAction
    }
}

Comments

0

Here is an approach that uses Kotlin reflection and extension functions to call copy() on members. The syntax isn't as clean as copy() but close.

If we have these data classes:

data class Foo(
    val name: String,
)
data class Bar(
    val id: Long,
    val foo: Foo,
)

then the code looks like this:

val template = Bar(...)
val copy = template.copy(id = 1)
    .copyFoo(Foo::name to "new name")

The extension function:

fun Bar.copyFoo(
    vararg overrides: Pair<KProperty1<Foo, *>, Any>
) {
    val oldFoo = this.foo
    val newFoo = copyMember<T>(oldFoo, *overrides)
    return this.copy(
       foo = newFoo
    )
}

or compact:

fun Bar.copyFoo(
    vararg overrides: Pair<KProperty1<Foo, *>, Any>
) {
    return this.copy(
       foo = copyMember<T>(this.foo, *overrides)
    )
}

The generic copyMember function looks like this.

/**
 * Dynamically copy a non-primitive member.
 *
 * Duplicate overrides are merged by keeping the last one.
 */
inline fun <reified T> copyMember(
    member: T,
    // The type T makes sure that all properties are members of the same type
    vararg overrides: Pair<KProperty1<T, *>, Any>
): T {
    val lookup = overrides.associateBy(
        { it.first.name },
        { it.second }
    )
    // Find the copy function of the member type
    val copyFn = T::class.memberFunctions.single { it.name == "copy" }
    // The copy function has an additional hidden parameter which contains "this" during the copy operation
    val instanceParam = copyFn.instanceParameter!!
    // These are the usual parameters for copy()
    val overrideParameters = copyFn.parameters
        .filter {
            lookup.containsKey(it.name)
        }
        .map {
            it to lookup[it.name]
        }
    val parameters = (listOf(instanceParam to member) + overrideParameters)
        .toMap()

    // Call copy with the instance and the overrides
    return copyFn.callBy(parameters) as T
}

The code relies a lot on type inference and internal checks in copy() to make sure the output is valid.

It's faster than serializing the classes but needs more code.

Comments

-1

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
    }

1 Comment

The same issue with this code as described in comments stackoverflow.com/a/58850583/3649629
-1

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)
    }

1 Comment

The same issue with this code as described in comments stackoverflow.com/a/58850583/3649629

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.