A kotlin.collections.List
is statically read-only (i.e. defines no modifying methods) but may be mutable at runtime. On the JVM:
val readonlyList: kotlin.collections.List<String> =
listOf("a", "b", "c") // At runtime: java.util.Arrays$ArrayList (which is a java.util.List)
As long as you stay in Kotlin land, readonlyList
is practically immutable, and when you can be confident that a non-malicious function f(list: kotlin.collections.List<T>)
will not mutate it when calling f(readonlyList)
. (A malicious function could of course cast and modify it, but then it could also access private variables via reflection).
A kotlin.collections.List
cannot be assigned to java.util.List
or java.util.ArrayList
, which makes sense given that these are generally mutable:
fun main() {
val a: java.util.List<String> = mutableList // Compile error: Type mismatch.
val b: java.util.ArrayList<String> = mutableList // Compile error: Type mismatch.
modify(readonlyList) // Compile error: Type mismatch.
}
fun <T> modify(list: java.util.List<T>) {
list.clear()
}
What baffles me is the decision to allow kotlin.collections.List
to be passed to Java methods that accept java.util.List
, i.e. methods that accept platform types:
val readonlyList: kotlin.collections.List<String> = listOf("a", "b", "c")
java.util.Collections.shuffle(readonlyList) // accepts java.util.List
println(readonlyList) // E.g., can print [a, c, b]
This is quite dangerous. I guess the motivation for this is better interoperability. Most Java methods that accept java.util.List
and that you want to use will probably not modify the parameter. E.g.:
val readonlyList: kotlin.collections.List<String> = listOf("a", "b", "c")
java.util.Collections.unmodifiableList(readonlyList) // accepts java.util.List
println(readonlyList) // Prints [a, b, c]
If kotlin.collections.List
was not accepted, but only the correct kotlin.collections.MutableList
, one would need to write java.util.Collections.unmodifiableList(readonlyList.toMutableList())
.
I am not sure that benefit of saving us from writing f(readonlyList as MutableList<String>)
or f(readonlyList.toMutableList())
is worth the potential of hard-to-find bugs that can arise when a list is modified and passed around, but you and the compiler reason about the code thinking it is read-only.
One could use truly immutable lists, which still do not provide type safety at compile time, but at least at runtime, thus making any bugs easy to find:
val immutableList: kotlinx.collections.immutable.ImmutableList<String> =
kotlinx.collections.immutable.persistentListOf("a", "b", "c")
java.util.Collections.shuffle(immutableList)
println(immutableList) // throws java.lang.UnsupportedOperationException: Operation is not supported for read-only collection
Not only is runtime-only type safety unsatisfying, but kotlin.collections.List
is already established, so this causes interoperability issues with Kotlin itself. E.g.:
fun processList(list: kotlin.collections.List<String>): kotlin.collections.List<String> =
list.filter { it == "foo" }
fun main() {
val immutableList: kotlinx.collections.immutable.ImmutableList<String> =
kotlinx.collections.immutable.persistentListOf("a", "b", "c")
java.util.Collections.shuffle(
processList(immutableList).toImmutableList() // must call .toImmutableList() for runtime type safety
)
}
So, as it is, one must be very careful when passing kotlin.collections.List
to Java methods. One must either trust that the names do not betray (always clearly indicate modification) or always remember to append toImmutableList()
to these arguments, potentially with a performance hit.
I wonder if it would be good if there would be a “strict mode” where in general only kotlin.collections.MutableList
is accepted for Java methods. To improve interoperability, there could also be a hybrid approach:
- Passing a
kotlin.collections.List
could still be allowed for a set of widely-used Java methods that promise to not modify the list. - Or in the current mode - where passing
kotlin.collections.List
is generally allowed - forbidding it for a set of Java methods that have a strong indicator of modifying it: If the method accepts only a list and its Javadoc documents ajava.lang.UnsupportedOperationException
. (Still an unsafe approach in general of course - just a mitigation.)
What do you think? Would such a compiler option makes sense? Is it considered best practice nowadays to use kotlinx.collections.immutable.ImmutableList
wherever possible (and still have no compile time safety)? Are there static checkers already that can provide the desired safety?
For simplicity, I have spoken only about lists, but the same issues apply to other collection types as well.