Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Defensive copy of a mutable collection in Kotlin data class

I want to have a data class accepting a read-only list:

data class Notebook(val notes: List<String>) {
}

But it can accept MutableList as well, because it is a subtype of the List.

For example the following code modifies the passed in list:

fun main(args: Array<String>) {
    val notes = arrayListOf("One", "Two")
    val notebook = Notebook(notes)

    notes.add("Three")

    println(notebook)       // prints: Notebook(notes=[One, Two, Three])
}

Is there a way how perform defensive copy of the passed in list in the data class?

like image 932
jbrachtl Avatar asked Jun 08 '17 12:06

jbrachtl


People also ask

Are Kotlin data classes mutable?

Kotlin Data Classes With Kotlin's data classes, you don't need to write/generate all the lengthy boilerplate code yourself. The compiler automatically generates a default getter and setter for all the mutable properties, and a getter (only) for all the read-only properties of the data class.

Can data class be abstract Kotlin?

All primary constructor parameters need to be marked as val or var . Data classes cannot be abstract, open, sealed, or inner.

Can Kotlin data class have methods?

Data classes specialize in holding data. The Kotlin compiler automatically generates the following functionality for them: A correct, complete, and readable toString() method. Value equality-based equals() and hashCode() methods.

What are data classes in Kotlin?

A Kotlin Data Class is used to hold the data only and it does not provide any other functionality apart from holding data. There are following conditions for a Kotlin class to be defined as a Data Class: The primary constructor needs to have at least one parameter.

What is the use of mutable list in Kotlin?

The kotlin mutable list is one of the collection interface, and it is used to create the list of datas that can be changed depends on the requirement. The generic collection of the data elements it will be inherited from the collection<T> class; the methods of the MutableList interface supports both readable and writeable functionalities.

What is a data class in Kotlin?

It is not unusual to create classes whose main purpose is to hold data. In such classes, some standard functionality and some utility functions are often mechanically derivable from the data. In Kotlin, these are called data classes and are marked with data: data class User(val name: String, val age: Int)

Is there a way to do a deep copy in Kotlin?

Which means that the copy in Kotlin's data class is a shallow copy. Is there a way to do a deep copy? Use List<String> instead of ArrayList<String>, and the need for a deep copy vanishes, since a List is immutable.

What is mutable list interface in Java?

The mutable list interfaces and is one of the generic collections using the data elements. It is one of mutable nature, and it is inherited from the collection<T> class; the methods of the mutablelist interface will support for both read and write functionalities.


2 Answers

Option 1

You can use .toList() to make a copy, although you will need another property internally that holds the list copy or you need to move away from a data class to a normal class.

As a data class:

data class Notebook(private val _notes: List<String>) {
    val notes: List<String> = _notes.toList()
}

The problem here is that your data class is going to have .equals() and .hashCode() based on a potentially mutating list.

So the alternative is to use a normal class:

class Notebook(notes: List<String>) {
    val notes: List<String> = notes.toList()
}

Option 2

Kotlin team is working on truly immutable collections as well, you might be able to preview them if they are stable enough for use: https://github.com/Kotlin/kotlinx.collections.immutable


Option 3

Another way would be to create an interface that does allow the descendant type of MutableList to be used. This is exactly what the Klutter library does by creating a hierarchy of light-weight delegating classes that can wrap lists to ensure no mutation is possible. Since they use delegation they have little overhead. You can use this library, or just look at the source code as an example of how to create this type of protected collections. Then you change your method to ask for this protected version instead of the original. See the source code for Klutter ReadOnly Collection Wrappers and associated tests for ideas.

As an example of using these classes from Klutter, the data class would be:

data class Notebook(val notes: ReadOnlyList<String>) {

And the caller would be forced to comply by passing in a wrapped list which is pretty simple:

val myList = mutableListOf("day", "night")
Notebook(myList.toImmutable())  // copy and protect

What is happening is that the caller (by invoking asReadOnly()) is making the defensive copy to satisfy the requirements of your method, and there is no way to then mutate the protected copy because of how these classes are designed.

One flaw in the Klutter implementation is that it does not have a separate hierarchy for ReadOnly vs. Immutable so if the caller instead calls asReadOnly() the holder of the list can still cause mutation. So in your version of this code (or an update to Klutter) it would be best to make sure all of your factory methods always make a copy and never allow these classes to be constructed in any other way (i.e. make constructors internal). Or have a second hierarchy that is used when the copy has clearly been made. The simplest way is to copy the code to your own library, remove the asReadOnly() methods leaving only toImmutable() and make the collection class constructors all internal.


Further information

see also: Kotlin and Immutable Collections?

like image 185
6 revs Avatar answered Sep 30 '22 15:09

6 revs


I think better is to use JetBrains library for immutable collections - https://github.com/Kotlin/kotlinx.collections.immutable

Import in your project

Add the bintray repository:

repositories {
    maven {
        url "http://dl.bintray.com/kotlin/kotlinx"
    }
}

Add the dependency:

compile 'org.jetbrains.kotlinx:kotlinx-collections-immutable:0.1'

Result:

data class Notebook(val notes: ImmutableList<String>) {}

fun main(args: Array<String>) {
    val notes = immutableListOf("One", "Two")
    val notebook = Notebook(notes)

    notes.add("Three") // creates a new collection

    println(notebook)       // prints: Notebook(notes=[One, Two])
}

Note: You also can use add and remove methods with ImmutableList, but this methods doesn't modify current list just creates new one with your changes and return it for you.

like image 37
Silvestr Avatar answered Sep 30 '22 14:09

Silvestr