Baeldung Pro – Kotlin – NPI EA (cat = Baeldung on Kotlin)
announcement - icon

Learn through the super-clean Baeldung Pro experience:

>> Membership and Baeldung Pro.

No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.

1. Overview

In this short tutorial, we’re going to learn how to copy a List in Kotlin.

2. Copying a List

In order to copy a List in Kotlin, we can use the toList() extension function:

val cities = listOf("Berlin", "Munich", "Hamburg")
val copied = cities.toList()
assertThat(copied).containsAll(cities)

As shown above, this function creates a new List and adds all the elements of the source List to it, one after another. Similarly, we can use the toMutableList() extension function to create a mutable collection:

val mutableCopy = cities.toMutableList()
assertThat(mutableCopy).containsAll(cities)

Both extension functions are creating a copy from the source. The only difference is that the latter returns a MutableList after copying.

Notably, these are only creating a shallow copy of the source List. That is, the two List instances are different objects in the Java heap, but their contents are the same objects:

assertThat(copied).isNotSameAs(cities)
assertThat(copied[0]).isSameAs(cities[0])

3. Deep Copying a List 

Lists can contain either primitive types (like Int or Char), or reference types (object instances of classes). If the List contains primitive types, a shallow copy is sufficient to make it unique and effectively like a deep copy. If the List contains reference types, each mutable element must be deep copied and added to a new List to create a true deep copy of the original list.

A deep copy of an object contains new instances of every embedded attribute. There is no general solution to always produce deep copies, but we’ll explore some common approaches that can help.

3.1. Deep Copying With Data Classes

If the List‘s type is a data class that only contains primitive types, the easiest way to deep copy is to use the built-in copy() method:

data class Person(var name: String, var age: Int)

val persons = listOf(Person("Bob", 20), Person("Alice", 21))
val personsCopy = persons.map { it.copy() }

assertThat(personsCopy).isNotSameAs(persons)
assertThat(personsCopy[0]).isNotSameAs(persons[0])

We copy() each element in the persons list via the map() function, which places the results into a new list. Stringsaren’t primitives but can be treated like them while cloning since they’re immutable. Finally, isNotSameAs() checks that the two memory references are different objects, similar to using !==.

However, each of the data class’s fields will only be shallow copied. So if the data class only contains primitives or immutable types, this works great. If the data class contains non-primitives or mutable references, we need to handle these differently.

Even if the non-primitive fields are also data classes, the copy() method won’t recursively copy those fields for us.

3.2. Deep Copying With a Custom Method

For scenarios where the collection type contains complex objects with more than primitive fields, one approach is to define a clone() method to produce a deep clone.

We’re using the clone() method name but aren’t committing to satisfying the Cloneable interface in these examples. The important thing to note is that every non-primitive member type of the collection type has to support deep cloning:

data class Address(var streetName: String, var streetNumber: Int, var city: String)

data class Person(val name: String, val age: Int, val address: Address) {
  fun clone(): Person {
    return Person(this.name, this.age, this.address.copy())
  }
}

val address1 = Address("Sesame Street", 1, "CartoonLand")
val address2 = Address("Sesame Street", 2, "CartoonLand")
val persons = listOf(Person("Bob", 20, address1), Person("Alice", 21, address2))

val personsCopy = persons.map { it.clone() }

assertThat(personsCopy).isNotSameAs(persons)
assertThat(personsCopy[0]).isNotSameAs(persons[0])
assertThat(personsCopy[0].address).isNotSameAs(persons[0].address)
assertThat(personsCopy[0].address).isEqualTo(persons[0].address)

personsCopy[0].address.streetNumber = 10

assertThat(personsCopy[0].address).isNotEqualTo(persons[0].address)

As we can see, the Person class has some primitive fields and a non-primitive Address field. Since Address is a data class with primitive and String fields, we can use the data class’s copy() method to clone it. If it had other class-type fields, we’d also have to define a custom clone() method in Address.

To clone a Person instance, we define a custom clone() function that copies the String and Int types directly and then sets a copy of the address field. Even though Person is a data class, we can’t rely on its copy() method for a deep copy because it reuses the same Address instance when producing the copy. After we’ve copied the address and updated the streetNumber of the new one, it no longer equals the original address.

3.3. Deep Cloning Collections of Immutable Types

We mentioned earlier that Strings aren’t primitives, but can be treated that way for cloning because they’re immutable. How about other immutable types?

We can notice that the Address data class in the example above is mutable, so creating a deep copy of it is important when deep copying a Person. Otherwise, modifications to one Person’s address can impact a cloned Person’s address, too, and vice-versa. That’s usually what we want to avoid by deep copying.

However, if the object properties are immutable, we can usually get away with a plain shallow copy. The shallow copy reuses the same references to object instances but the properties are immutable and therefore can’t be changed. Kotlin makes defining immutable types easy using the val keyword. So if that’s all we care about, we don’t need custom implementations to clone immutable types.

4. Conclusion

In this article, we learned a couple of ways to copy the contents of a List to another one in Kotlin, some ways to deep clone non-primitive Lists, and the benefits of using immutable types when cloning.

The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.