首页 > 解决方案 > 为什么某些 Java 函数能够更改不可变的 Kotlin 对象?

问题描述

我惊讶地发现这个程序甚至可以编译,但结果更让我惊讶:

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)

产生以下错误消息:

类型推断失败:有趣的填充(p0:MutableList!,p1:T!):单位不能应用于(List,Int)类型不匹配:推断类型是List但MutableList!预计

然而,函数的List参数fill具有不同的类型界限reverse

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

public static void reverse(List<?> list)

所以似乎没有类型限制,Java 函数可以做任何他们想做的事情。

有人可以解释这怎么可能?这是设计使然,还是只是互操作的限制?

标签: javakotlincollectionsinterop

解决方案


不编译的函数与 kotlin 无关,而是 java 如何处理协变和逆变集合。

来自Java 泛型和集合

您不能将任何内容放入使用 extends 通配符声明的类型中——除了 null 值,它属于每个引用类型

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

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

如果 1编译器不会让你这样做,因为编译器无法知道列表的确切类型,在这种情况下,它是整数列表,在其他情况下,它可能会根据某个运行时分配一个双精度列表健康)状况。

情况 2中,编译器无法确认插入到列表中的对象的类型,并产生错误。您可以使用Wildcard Capture解决案例 2 中的问题。

其次是您关于在 java 方法中改变 kotlin 列表的问题。

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

现在,鉴于 java 没有像 kotlin 那样的可变列表和只读列表的任何概念,因此无法阻止 java 方法改变您的只读 kotlin 列表。因为对于只是一个列表实现的java代码。

以下是 Kotlin in Action一书中的相关文字

当您需要调用 Java 方法并将集合作为参数传递时,您可以直接执行此操作,无需任何额外步骤。例如,如果您有一个将 java.util.Collection 作为参数的 Java 方法,则可以将任何 Collection 或 Mutable Collection 值作为参数传递给该参数。

这对集合的可变性有重要影响。因为 Java 不区分只读集合和可变集合,Java 代码可以修改集合,即使它在 Kotlin 端被声明为只读集合。Kotlin 编译器无法完全分析 Java 代码中对集合所做的操作,因此 Kotlin 无法拒绝将只读集合传递给修改它的 Java 代码的调用


推荐阅读