Why can kotlin.collections.List be passed to method that accepts java.util.List?

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.

3 Likes

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.

3 Likes

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.