Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why are some Java functions able to change an immutable Kotlin object?

I was surprised to see that this program even compiles, but the result surprised me even more:

import java.util.Collections.swap

fun main(args: Array<String>) 
{
    val immutableList = List(2) { it } // contents are [0, 1] 
    swap(immutableList, 0, 1)
    println(immutableList) // prints [1, 0]
}

The swap function is implemented in the library as:

public static void swap(List<?> list, int i, int j) {
        list.set(i, list.set(j, list.get(i)));
    }

where the List is a mutable Java list, not an immutable Kotlin one. So I thought that other Java functions will work as well. For instance:

reverse(immutableList)

works, but others, such as the fill function, does not even compile:

fill(immutableList, 3)

produces the following error message:

Type inference failed: fun fill(p0: MutableList!, p1: T!): Unit cannot be applied to (List,Int) Type mismatch: inferred type is List but MutableList! was expected

The List argument of the fill function however has different type bounds than reverse:

public static <T> void fill(List<? super T> list, T obj)

public static void reverse(List<?> list)

so it seems that without the type bounds, the Java functions can do whatever they want.

Can someone explain how is this possible? Is this by design, or is it just a limitation of the interop?

like image 642
Martin Drozdik Avatar asked Dec 18 '19 14:12

Martin Drozdik


2 Answers

functions that doesn't compile has nothing to do with kotlin but how covariant and contravariant collections are handled by java.

from Java Generics and Collections

You cannot put anything into a type declared with an extends wildcard—except for the value null, which belongs to every reference type

for example if you have following code.

List<? extends Number> numbers = new ArrayList<Integer>();

Then you can do this

 numbers.add(null);

but if you tried to do any of the following

numbers.set(0, Integer.valueOf(10)); // case 1
numbers.set(1, numbers.get(0)); // case 2

in case 1 compiler will not let you do it because there is no way for the compiler to know the exact type of list, in this case its a list of integers in some other case it may be assigned a list of doubles based on some runtime condition.

in case 2 the compiler is not able to confirm the type of object that is being inserted into the list, and an error is produced. you can solve the problem in case 2 using Wildcard Capture.

Second is your question about mutating kotlin lists in java methods.

we have to understand that kotlin collections classes are same old java collection classes and kotlin doesn't have its own collections implementation. what kotlin does is that it divides the java collection interfaces into mutable and read only. if you create an array list in kotlin and then check its class you will get same java.util.ArrayList

Now given that java doesn't have any notion of mutable list and read only list as kotlin does, there is no way to stop java methods from mutating your read only kotlin list. because for java code that is just a list implementation.

following is the relevant text from book Kotlin in Action

When you need to call a Java method and pass a collection as an argument, you can do so directly without any extra steps. For example, if you have a Java method that takes a java.util.Collection as a parameter, you can pass any Collection or Mutable Collection value as an argument to that parameter.

This has important consequences with regard to mutability of collections. Because Java doesn’t distinguish between read-only and mutable collections, Java code can modify the collection even if it’s declared as a read-only Collection on the Kotlin side. The Kotlin compiler can’t fully analyze what’s being done to the collection in the Java code, and therefore there’s no way for Kotlin to reject a call passing a read-only Collection to Java code that modifies it.

like image 65
mightyWOZ Avatar answered Oct 13 '22 21:10

mightyWOZ


It should be noted that your version of swap is not compileable. It is the result of decompiling the actual library code that casts the List to a raw type so it can read and write with it despite the wildcard.

The swap method uses a wildcard type, and so there is no limitation between the interface mapping in the interop. Kotlin maps both List and MutableList to Java's List, so it unfortunately allows misuse like this. I suppose it is necessary to avoid preventing use of Java libraries.

Your fill method uses a contravariant (consumer) bounded type <? super T> which is incompatible with Kotlin's List. Kotlin's List is declared with a covariant (producer) type only: List<out T>. So they are not a match for each other.

like image 1
Tenfour04 Avatar answered Oct 13 '22 21:10

Tenfour04