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?
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.
All primary constructor parameters need to be marked as val or var . Data classes cannot be abstract, open, sealed, or inner.
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.
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.
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.
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)
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.
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.
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()
}
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
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
.
see also: Kotlin and Immutable Collections?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With