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 a java.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.
marcoeckstein:
What baffles me is the decision to allow kotlin.collections.List
to be passed to Java methods that accept java.util.List
That is not entirely true, you cannot pass a kotlin.collections.List
into a Kotlin method taking java.util.List
, just to a pure java class taking a java.util.List
.
Consider the code below:
val kotlinList: kotlin.collections.List<String> = listOf("a", "b", "c")
val javaList: java.util.List<String> = kotlinList // Type mismatch.
fun takesJavaList(l: java.util.List<String>) {
TODO()
takesJavaList(kotlinList) // Type mismatch.
java.util.Collections.shuffle(kotlinList) // warning: Call of Java mutator 'shuffle' on immutable Kotlin collection 'kotlinList'
There i no better option, really, the pain of not letting any kotlin lists be used in Java would certainly overweight the extra safety by making kotlin.collections.List
and java.util.List
totally incompatible.
Here’s another idea: have a compiler flag to insert Collections.unmodifiableList
anytime a Kotlin read-only List
is passed to a Java function that accepts a java.util.List
.
This way, you get the safety you mentioned, with behavior consistent with the Java standard library, without needing to write more code.
brunojcm:
That is not entirely true, you cannot pass a kotlin.collections.List
into a Kotlin method taking java.util.List
, just to a pure java class taking a java.util.List
.
Yes, that’s why I was referring to Java methods specifically.
brunojcm:
java.util.Collections.shuffle(kotlinList) // warning: Call of Java mutator 'shuffle' on immutable Kotlin collection 'kotlinList'
Interesting. For some strange reason I had not seen that warning before. That is essentially the second hybrid approach that I had suggested - forbidding/discouraging passing a kotlin.collections.List
to a Java method that is know to mutate. Do you know how the static analysis knows this?
clovisai:
have a compiler flag to insert Collections.unmodifiableList
anytime a Kotlin read-only List
is passed to a Java function that accepts a java.util.List
.
That would be nice.
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()
Instead of mutableList
, it should have been readonlyList
, even though indeed the same compile errors would pop up when using a kotlin.collections.MutableList
.
marcoeckstein:
Interesting. For some strange reason I had not seen that warning before. That is essentially the second hybrid approach that I had suggested - forbidding/discouraging passing a kotlin.collections.List
to a Java method that is know to mutate. Do you know how the static analysis knows this?
I’m no expert in the Kotlin compiler, but I’d say it’s probably an IDE warning or a list of hardcoded methods somewhere.