Issue
I was writing a small piece of code in which I internally handle my data in a mutable map, which in turn has mutable lists.
I wanted to expose my data to the API user, but to avoid any unsafe publication of my data I wanted to expose it in immutable collections even when internally being handled by mutable ones.
class School {
val roster: MutableMap<Int, MutableList<String>> = mutableMapOf<Int, MutableList<String>>()
fun add(name: String, grade: Int): Unit {
val students = roster.getOrPut(grade) { mutableListOf() }
if (!students.contains(name)) {
students.add(name)
}
}
fun sort(): Map<Int, List<String>> {
return db().mapValues { entry -> entry.value.sorted() }
.toSortedMap()
}
fun grade(grade: Int) = db().getOrElse(grade, { listOf() })
fun db(): Map<Int, List<String>> = roster //Uh oh!
}
I managed to expose only Map
and List
(which are immutable) in the public API of my class, but the instances I am actually exposing are still inherently mutable.
Which means an API user could simply cast my returned map as an ImmutableMap and gain access to the precious private data internal to my class, which was intended to be protected of this kind of access.
I couldn't find a copy constructor in the collection factory methods mutableMapOf()
or mutableListOf()
and so I was wondering what is the best and most efficient way to turn a mutable collection into an immutable one.
Any advice or recommendations?
Solution
Currently in Kotlin stdlib there are no implementations of List<T>
(Map<K,V>
) that would not also implement MutableList<T>
(MutableMap<K,V>
). However due to Kotlin's delegation feature the implementations become one liners:
class ImmutableList<T>(private val inner:List<T>) : List<T> by inner
class ImmutableMap<K, V>(private val inner: Map<K, V>) : Map<K, V> by inner
You can also enhance the creation of the immutable counterparts with extension methods:
fun <K, V> Map<K, V>.toImmutableMap(): Map<K, V> {
if (this is ImmutableMap<K, V>) {
return this
} else {
return ImmutableMap(this)
}
}
fun <T> List<T>.toImmutableList(): List<T> {
if (this is ImmutableList<T>) {
return this
} else {
return ImmutableList(this)
}
}
The above prevents a caller from modifying the List
(Map
) by casting to a different class. However there are still reasons to create a copy of the original container to prevent subtle issues like ConcurrentModificationException:
class ImmutableList<T> private constructor(private val inner: List<T>) : List<T> by inner {
companion object {
fun <T> create(inner: List<T>) = if (inner is ImmutableList<T>) {
inner
} else {
ImmutableList(inner.toList())
}
}
}
class ImmutableMap<K, V> private constructor(private val inner: Map<K, V>) : Map<K, V> by inner {
companion object {
fun <K, V> create(inner: Map<K, V>) = if (inner is ImmutableMap<K, V>) {
inner
} else {
ImmutableMap(hashMapOf(*inner.toList().toTypedArray()))
}
}
}
fun <K, V> Map<K, V>.toImmutableMap(): Map<K, V> = ImmutableMap.create(this)
fun <T> List<T>.toImmutableList(): List<T> = ImmutableList.create(this)
While the above is not hard to implement there are already implementations of immutable lists and maps in both Guava and Eclipse-Collections.
Answered By - miensol
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.