The following is quick explanation on the differences between shallow and deep copy, and both can be used in Kotlin.
https://blog.protein.tech/kotlin-shallow-vs-deep-copy-explained-907a72ccbf7a
In this article, I’ll explain the difference between deep and shallow copying in Kotlin and why it’s crucial to understand it when copying data classes and lists.
Shallow Copy
A shallow copy creates a new object that is a copy of the original object, but it does not create new copies of the nested objects. Instead, the new object simply contains references to the same nested objects as the original object.
Confused? Let me explain it with an example :)
To shallow copy a data class in Kotlin, we can use copy()
function
data class Address(var city: String)
data class User(val name: String, val surname: String, val address: Address)
val address = Address("New York")
val originalUser = User("John", "Smith", address)
val copiedUser = originalUser.copy(name = "Ilyas")
println(originalUser) // User(name=John, surname=Smith, address=Address(city=New York))
println(copiedUser) // User(name=Ilyas, surname=Smith, address=Address(city=New York))
// Change the city in original user's address
originalUser.address.city = "San Francisco"
println(originalUser) // User(name=John, surname=Smith, address=Address(city=San Francisco))
println(copiedUser) // User(name=Ilyas, surname=Smith, address=Address(city=San Francisco))
Note that after copying the user, only the name was altered, and everything else remained the same. However, when we modified the city in the originalUser
, the copiedUser
was also affected
This happens because copy function didn’t copy the nested objects values (address) instead it just assigned the reference of the address to copiedUser
.
If you took a look at the copy implementation it would make total sense…
fun copy(
name: String = this.name,
surname: String = this.surname,
address: Address = this.address
): User = User(name, surname, address)
Note: We wouldn’t face this problem if all of the data class’s properties are immutables. That’s one of the reasons to always have immutable properties.
Another thing to notice is copy
function only copies the properties in the primary constructor. That means that it won’t copy any property you define inside the class body.
data class User(val name: String, ...){
var isChanged = false
}
val originalUser = User("John", ...)
originalUser.isChanged = true
val copiedUser = originalUser.copy(name = "Ilyas")
println(originalUser.isChanged) // true
println(copiedUser.isChanged) // surprise surprise, it's false 🙂
This is normally a preferred behavior but you need to be careful about it.
Deep copy
A deep copy creates a new object that is a copy of the original object and (recursively) all its nested objects. Therefore, any changes made to the nested objects in the original object will not affect the new object.
Kinda abstract right? Let me show you examples…
To achieve deep copy in Kotlin you can implement Cloneable
interface and provide a custom implementation for it.
data class Address(var city: String): Cloneable {...}
data class User(val name: String, val address: Address): Cloneable{
var isChanged = false
public override fun clone(): User {
return User(this.name, this.address.clone()).also {
it.isChanged = this.isChanged
}
}
}
This implementation can be hard and requires a lot of boilerplate code, (imagine having 10 nested properties). Instead, we can use Json to convert the class to String and then use it to create a new deep-copied object.
data class User(val name: String, val address: Address): Cloneable {
var isChanged = false
public override fun clone(): User {
return gson.toJson(this).let { gson.fromJson(it, User::class.java) }
}
}
val address = Address("New York")
val originalUser = User("John", address)
originalUser.isChanged = true
val clonedUser = originalUser.clone()
originalUser.address.city = "San Francisco"
println("Original: $originalUser, isChanged: ${originalUser.isChanged}")
// Original: User(name=John, address=Address(city=San Francisco)), isChanged: true
println("Cloned: $clonedUser, isChanged: ${clonedUser.isChanged}")
// Cloned: User(name=John, address=Address(city=New York)), isChanged: true
Note that we didn’t encounter any of the issues that we faced in the shallow copy (changing the city
of the originalUser
didn’t affect the clonedUser
+ isChanged
copied successfully) 🤩💃
Finally here is how you copy a list of objects.
If all of your data class’s properties are immutable…
val users = listOf<User>(User(...), User(...), ...) val copy1 = users.toList() // or toMutableList() val copy2 = users.map { it.copy() }
If you have any mutable properties…
val users = listOf<User>(User(...), User(...), ...) val copied = users.map { it.clone() // where clone() makes a deep copy of the item }
Conclusion
Although deep copying creates a complete replica of an object, it can be slower and requires more code compared to shallow copying. Therefore, I recommend using data classes with immutable properties over deep copying. And I hope you loved the blog 💜