为什么某些 Java 函数能够更改不可变的 Kotlin 对象?

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

我很惊讶这个程序竟然能通过编译,但结果更让我吃惊:

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]
}

swap函数在库中实现为:

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

其中 List 是可变的 Java 列表,而不是不可变的 Kotlin 列表。所以我认为其他 Java 函数也可以。例如:

reverse(immutableList)

有效,但是 fill 函数等其他函数甚至无法编译:

fill(immutableList, 3)

产生以下错误消息:

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

fill 函数的 List 参数与 reverse:

的类型范围不同
public static <T> void fill(List<? super T> list, T obj)

public static void reverse(List<?> list)

所以似乎没有类型限制,Java 函数可以为所欲为。

谁能解释一下这怎么可能?这是设计使然,还是仅仅是互操作的限制?

不编译的函数与 kotlin 无关,但 java.

如何处理协变和逆变集合

来自 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

例如,如果您有以下代码。

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

那么你可以这样做

 numbers.add(null);

但如果您尝试执行以下任一操作

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

case 1 编译器不会让你这样做,因为编译器无法知道列表的确切类型,在这种情况下它是一个整数列表在其他一些情况下,它可能会根据某些运行时条件分配一个双打列表。

情况 2 中,编译器无法确认插入到列表中的对象的类型,因此会产生错误。您可以使用 Wildcard Capture.

解决案例 2 中的问题

第二个是关于在 java 方法中改变 kotlin 列表的问题。

我们必须了解 kotlin 集合 classes 与旧的 java 集合 classes 相同,kotlin 没有自己的集合实现。 kotlin 所做的是将 java 集合接口分为可变和只读。 如果你在 kotlin 中创建一个数组列表然后检查它的 class 你会得到相同的 java.util.ArrayList

既然 java 不像 kotlin 那样有任何可变列表和只读列表的概念,就没有办法阻止 java 方法改变你的只读 kotlin 列表。因为 java 代码只是一个列表实现。

以下是书中的相关文字 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.

需要注意的是你的swap版本是不可编译的。这是反编译将 List 转换为原始类型的实际库代码的结果,因此尽管有通配符,它​​仍可以使用它进行读写。

swap方法使用通配符类型,因此interop中的接口映射之间没有限制。 Kotlin 将 List 和 MutableList 都映射到 Java 的 List,因此不幸的是它允许这样的滥用。我想有必要避免阻止使用 Java 库。

您的 fill 方法使用了与 Kotlin 列表不兼容的逆变(消费者)有界类型 <? super T>。 Kotlin 的列表仅使用协变(生产者)类型声明:List<out T>。所以他们不是一对。