1. Overview
In general terms, cloning is the process of creating an identical copy of an object. Meanwhile, in a programming context, cloning means creating a new object that has the same values and properties as the original object.
In this article, we’ll discuss approaches that can be used to clone objects in Kotlin.
2. Shallow Versus Deep Copy
Before discussing how to clone an object, we should first understand the concepts of shallow copy and deep copy properly. This is especially important if we are dealing with complex data structures such as nested objects or collections.
A shallow copy means we only copy references to existing objects, not the actual objects or values.
If we have an object with a complex schema, for example, having nested objects at multiple levels, and we only copy the top layer of fields, that is also a kind of shallow copy.
Meanwhile, deep copy means creating a copy that actually creates a new object for each field, not just a reference copy.
So, for complex schemas, a deep copy results in a data structure with new copies of all objects in the data structure at all layers.
3. Cloning Methods
We can clone objects using various approaches, depending on the complexity of the object we want to clone. To demonstrate this, we will create a model with a realistic schema to provide a clear example.
First, let’s define some classes to work with:
class Address(var street: String, var city: String)
class Person(var name: String, var address: Address)
class Company(var name: String, var industry: String, val ceo: Person, val employees: List<Person>)
class Organization(var name: String, val headquarters: Address, val companies: List<Company>)
We have an organization that includes companies in which there are people who work in them:
// ... other Company, Person and Address objects
val organization = Organization("Bekraf", Address("Jalan Medan Merdeka Selatan", "Jakarta"), listOf(companyBasen, companyKotagede))
3.1. Using Data Class copy()
Each data class in Kotlin has a built-in function copy(), which is used to create a copy of an object. By default, copy() performs a shallow copy.
But before that, we must change the classes we have into data classes by adding the data keyword before the class:
data class Address(var street: String, var city: String)
data class Person(var name: String, var address: Address)
data class Company(var name: String, var industry: String, val ceo: Person, val employees: List<Person>)
data class Organization(var name: String, val headquarters: Address, val companies: List<Company>)
We can then call the copy() function directly, but this would result in a shallow copy:
val clonedOrganization = organization.copy()
However, we can perform a deep copy if each nested object is also copied.
Let’s be thorough and careful to copy each of the fields, including those found in collections:
val clonedOrganization = organization.copy(
headquarters = organization.headquarters.copy(),
companies = organization.companies.map { company ->
company.copy(
ceo = company.ceo.copy(
address = company.ceo.address.copy()
),
employees = company.employees.map { employee ->
employee.copy(
address = employee.address.copy()
)
}
)
}
)
We perform a deep copy of the organization object by creating a new copy of headquarters and each companies, including a copy of ceo and each employees within each companies, along with their address.
3.2. Using clone()
We can also use the clone() method, which performs a shallow copy by default. However, this requires implementing Java’s Cloneable interface in the class.
To achieve a deep copy, we need to override the clone() method and make it public. Within the overridden method, we will create new copies for all the object’s properties and return the newly created object.
So let’s look at the changes to the classes we have:
data class Address(var street: String, var city: String) : Cloneable {
public override fun clone() = Address(this.street, this.city)
}
data class Person(var name: String, var address: Address) : Cloneable {
public override fun clone() = Person(name, this.address.clone())
}
data class Company(var name: String, var industry: String, val ceo: Person, val employees: List<Person>) : Cloneable {
public override fun clone() = Company(name, industry, ceo.clone(), employees.map { it.clone() })
}
data class Organization(var name: String, val headquarters: Address, val companies: List<Company>) : Cloneable {
public override fun clone() = Organization(name, headquarters.clone(), companies.map { it.clone() })
}
Yes, this requires more effort at the beginning. But now, calling clone() will easily perform a deep copy:
val clonedOrganization = organization.clone()
3.3. Using Secondary Constructor
A secondary constructor is an additional constructor that a class can have in addition to the primary constructor. As we know, constructors are typically used to create instances (objects) of a class. So, we can say this is a way to clone objects without involving data classes or Cloneable interfaces.
Let’s add a secondary constructor:
class Organization(var name: String, val headquarters: Address, val companies: List<Company>) {
constructor(organization: Organization) : this(
organization.name,
Address(organization.headquarters),
organization.companies.map { Company(it) })
}
// ... similar constructors for Address, Person and Company
In each class, we create a constructor that accepts the same type and uses its values in a newly created reference structure.
Now, to create a deep copy, we can easily call the constructor:
val clonedOrganization = Organization(organization)
3.4. Using Custom Deep Copy
If we want to separate the copy creation logic from the main constructor and be more flexible, then we can use this approach. Yes, we can create our own function:
class Organization(var name: String, val headquarters: Address, val companies: List) {
fun deepCopy(
name: String = this.name,
headquarters: Address = this.headquarters.deepCopy(),
companies: List = this.companies.map { it.deepCopy() },
) = Organization(name, headquarters, companies)
}
// ... similar deepCopy for Address, Person and Company
The deepCopy() function in each of these classes creates nested copies of objects. For example, in the Organization class, to copy the headquarters object, the deepCopy() function will essentially call deepCopy() from Address, which in turn will also call deepCopy() from its other properties.
Now, to create a deep copy, we just call:
val clonedOrganization = organization.deepCopy()
4. Validation
To validate whether an object is a deep copy or not, we can check whether changes to the original object do not affect the copied object.
Since this is a complex object, let’s do it in depth:
organization.name = "New Org Name"
organization.headquarters.city = "New City"
organization.companies.first().name = "New Company Name"
organization.companies.first().ceo.name = "New CEO Name"
organization.companies.first().ceo.address.city = "New CEO Address City Name"
organization.companies.first().employees.first().name = "New Employee Name"
organization.companies.first().employees.first().address.city = "New Employee Address City Name"
Let’s see if changes to the original object do not affect the copy object.
assertThat(clonedOrganization)
.isNotSameAs(organization)
assertThat(clonedOrganization.headquarters.city)
.isNotEqualTo("New City")
assertThat(clonedOrganization.companies.first().name)
.isNotEqualTo("New Company Name")
assertThat(clonedOrganization.companies.first().ceo.name)
.isNotEqualTo("New CEO Name")
assertThat(clonedOrganization.companies.first().ceo.address.city)
.isNotEqualTo("New CEO Address City Name")
assertThat(clonedOrganization.companies.first().employees.first().name)
.isNotEqualTo("New Employee Name")
assertThat(clonedOrganization.companies.first().employees.first().address.city)
.isNotEqualTo("New Employee Address City Name")
Yes, everything works fine.
5. Conclusion
In this tutorial, we have discussed approaches to object cloning. If we want something simple and expressive with a typical Kotlin style, then we can use the data class copy(), although we have to be careful with nested objects that are cloned by reference, not value.
Meanwhile, clone() is also possible but requires more effort at the beginning because we must implement the Cloneable interface.
Using a secondary constructor is an if we don’t want to depend on data classes or Cloneable interfaces. Our custom deepCopy offers similar flexibility but still requires effort and precision at the start.
As always, the complete code samples for this article can be found on GitHub.