Solution to data class list immutability problem thingy

Sooo the following code is suboptimal if i want immutability.

fun main() {
    val interests = mutableListOf("dancing")
    
    val person = Person("Mike", interests)
    println(person) // interests: ["dancing"]

    interests.add("killing") // we just mutated mike

    println(person) // interests: ["dancing", "killing"]
}

data class Person(val name: String, val interests: List<String>) // i want this to be completely immutable

Now what’s a good solution to that?
I’d want to do this but it is not allowed for data classes sadly

data class Person(val name: String, interests: List<String>) {
    val interests = interests.toList() // defensive copy
}

so the only solution i see is to make the constructor private and add a factory function. does anyone have a better idea to fix it?

@Suppress("DataClassPrivateConstructor")
data class Person private constructor(val name: String, val interests: List<String>) {
    companion object {
        fun create(name: String, interests: List<String>) = Person(name, interests.toList())
    }
}

edit: avoiding cloning while still guaranteeing immutability would of course be ideal but i doubt thats possible in kotlin since we have no such concept as deep (im)mutability

The best you could do I think is something like this right now:

@Suppress("DataClassPrivateConstructor")
data class Person private constructor(val name: String, val interests: List<String>) {
	companion object {
		@Suppress("KotlinConstantConditions")
		operator fun invoke(name: String, interests: List<String>) = Person(
			name = name,
			interests = if (interests is MutableList<String>) interests.toList() else interests
		)
	}
}

You can pretty much call it just like a constructor.

As for collections it depends on provided implementation. Unfortunatelly most of the standard collection types are mutable according to Kotlin’s type system, so copying could be avoided only if you implemented your own collection type:

class MyReadOnlyList : List<String> by emptyList()

The trick is that there is this internal KMappedMarker marker which gives an information that this is read-only collection, and can’t be cast as mutable one:

fun main() {
	val list = MyReadOnlyList()
	list as MutableList<String> // this will fail
}

I hope that this helped somewhat.

I combined @madmax1028’s solution with my own very questionable function to steal ownership from lists to make them truly immutable. This works, I can’t wait to use it in production.

import java.util.*
import kotlin.collections.ArrayList

fun main() {
    val interests = mutableListOf("dancing")

    val person = Person("Mike", interests)
    println(person)
    interests.add("killing")
    println(person)
}

@Suppress("DataClassPrivateConstructor")
data class Person private constructor(val name: String, val interests: List<String>) {
    companion object {
        operator fun invoke(name: String, interests: List<String>) = Person(name, interests.makeImmutable())
    }
}

fun <T> List<T>.makeImmutable(): List<T> {
    val list = if (this is ArrayList<T>) stealOwnership() else toList()
    return Collections.unmodifiableList(list)
}

private fun <T> ArrayList<T>.stealOwnership(): List<T> {
    val elementDataField = javaClass.getDeclaredField("elementData").apply { isAccessible = true }
    val sizeField = javaClass.getDeclaredField("size").apply { isAccessible = true }
    val emptyElementData = javaClass.getDeclaredField("EMPTY_ELEMENTDATA").apply { isAccessible = true }.get(null)

    val elementData = elementDataField.get(this)

    val clonedList = ArrayList<T>()

    elementDataField.set(clonedList, elementData)
    sizeField.set(clonedList, size)

    elementDataField.set(this, emptyElementData)
    sizeField.set(this, 0)

    return clonedList
}

What is this monstrosity? O_O What is wrong with a simple data copy? And what if someone will do something like this?

val person = Person("Mike", interests) // result: Person(name=Mike, interests=[dancing])
val person2 = Person("John", interests) // result: Person(name=John, interests=[])

Collections.unmodifiableList() also seems to be unnecessary, List is read-only already. If the user of Person is motivated enough to hack through the type system to modify what was designed to be unmodifiable, then they can use reflection or other means to modify even unmodifiableList().

Just throw away all this code, use simple toList() and you will be fine :slight_smile:

1 Like

I consider it basically impossible to enforce any level of encapsulation within a data class. The problem is that even if you make both the primary constructor and the properties private, they are still going to be effectively public thanks to the copy and componentN methods that are generated regardless. My recommendation would be to only use data classes for plain dumb data structures that don’t pressume to encapsulate anything anyway or have any important class invariants. If you want anything stronger than that (i.e. something that follows OOP principles), use a regular class rather than a data class. As for the problem with collection ownership, the compiler can already handle some of the cases thanks to the fact that Kotlin has read-only collection interfaces. For the remaining few cases, I would consider it most pragmatic to rely on disciplined programming practices to not unintentionally modify a collection after it has been passed to a data class.

Why not adopt an immutable and persistent List type such as the one here: GitHub - KenBarclay/Dogs: Library of persistent and immutable data structures

Why to use some random, unpopular and undocumented library if there is an “official” lib for Kotlin? GitHub - Kotlin/kotlinx.collections.immutable: Immutable persistent collections for Kotlin