Kotlin 一直宣称和 Java 完全兼容,无缝混编。大多数情况下确实如此。我从 2017 年 Android 宣称支持 Kotlin 开始,就一直学习并使用 Kotlin,并且将项目中很多 Java 代码都转成了 Kotlin,没出过什么问题。
直到最近,测试测出了一个 crash,我在调研之后发现这是 Java 和 Kotlin 混编才会导致的一个 bug。
代码并不复杂,简化出来大概是这么几行代码:
Java
public class JavaUtils {
public void addElement(List<String> list) {
list.add("d");
}
}
Kotlin
class KotlinTest {
@Test
fun test() {
val list = listOf("a", "b", "c")
JavaUtils().addElement(list)
}
}
这个 KotlinTest 中的 test 方法一运行,就会报 UnsupportedOperationException:
log
java.lang.UnsupportedOperationException
at java.base/java.util.AbstractList.add(AbstractList.java:153)
at java.base/java.util.AbstractList.add(AbstractList.java:111)
at com.example.myapplication.JavaUtils.addElement(JavaUtils.java:11)
at com.example.myapplication.KotlinUtils.test(KotlinUtils.kt:10)
at ...
可以看出,问题很简单,Kotlin 中的 listOf 创建的 List 对象是不可变的,将其传递到 Java 方法中,尝试为其添加元素,便会抛出 UnsupportedOperationException。将 listOf 换成 mutableListOf 就能解决了。
为什么说这是 Java 和 Kotlin 混编才会出现的问题呢?
因为纯 Kotlin 代码,这种写法在编译期就会报错了。
在 Kotlin 代码中,这两种写法都是过不了编译的:
Kotlin
class KotlinTest {
@Test
fun test() {
val list = listOf("a", "b", "c")
addElement(list)
}
fun addElement(list: List<String>) {
/*
* Unresolved reference.
* None of the following candidates is applicable because of a receiver type mismatch: fun String. add(s: String): String
*/
list.add("d")
}
}
Kotlin
class KotlinTest {
@Test
fun test() {
val list = listOf("a", "b", "c")
/*
* Error: Type mismatch.
* Required:
* MutableList<String>
* Found:
* List<String>
*/
addElement(list)
}
fun addElement(list: MutableList<String>) {
list.add("d")
}
}
第一种写法会报 List 不支持 add,第二种写法会报传入的参数类型不符合。
神奇的马甲
上文讲到的 bug 看起来很简单,但实际上,项目里的这个问题还套了一层马甲,才导致开发人员没有第一时间发现这个 bug,而这个马甲就有点意思了。
实际项目里的代码简化出来大概是这么几行代码:
Java
public final class JavaUtils {
public static void addElement(List<String> list) {
list.add("new-element");
System.out.println("list: " + list);
}
}
Kotlin
class KotlinTest {
@Test
fun test() {
val list = arrayOf("a", "b")
JavaUtils.addElement(list.toList())
}
}
JavaUtils 中有一个 addElement 方法,作用是向列表添加一个元素。KotlinTest 类得 test 方法中,先构建一个 Array 对象,然后调用 toList 将其转成列表,再调用 addElement 尝试为其添加元素。
请问:这个 test()
方法运行时会出问题吗?
答案是不会。
这里可能有读者要抽出大刀,问道:toList 这名字看起来是转成不可变列表啊,为什么不会?
先把刀收一收,看看下一个问题。
接下来我们将 test()
方法稍加改动:
Kotlin
class KotlinTest {
@Test
fun test() {
val list = arrayOf("a")
JavaUtils.addElement(list.toList())
}
}
唯一的改动是把 list 由 arrayOf("a", "b")
改成了 arrayOf("a")
,也就是减少了一个元素。
再请问:这个 test()
方法运行时会出问题吗?
答案是会。报的错是:
PlainText
java.lang.UnsupportedOperationException
at java.base/java.util.AbstractList.add(AbstractList.java:153)
at java.base/java.util.AbstractList.add(AbstractList.java:111)
at com.kevintest.mycomposedialogapplication.JavaUtils.addElement(JavaUtils.java:9)
at com.kevintest.mycomposedialogapplication.KotlinTest.test(KotlinTest.kt:10)
这个神奇的马甲就是:开发环境下,列表中有多个元素,所以开发人员没有遇到报错。测试人员在测的时候,配置了测试环境,将列表中的元素限制成了 1 个,就报出了 crash。
bug 原因也很简单,看一下 Array<out T>.toList()
源码就一目了然了:
kotlin
/**
* Returns a [List] containing all elements.
*/
public fun <T> Array<out T>.toList(): List<T> {
return when (size) {
0 -> emptyList()
1 -> listOf(this[0])
else -> this.toMutableList()
}
}
注:本例中采用的 Kotlin 版本是 org.jetbrains.kotlin:kotlin-stdlib:2.0.0
,不确定以后会不会修改这里。

这源码闪瞎了我的眼,如果数组长度为 0 或者 1,则 toList 创建一个不可变列表。否则创建一个可变列表。
如果我见了这个源码的编写人员,高低得问一句:

Look into my eyes! 回答我!Tell me why!
最后的解决方案是使用 toMutableList()
,这个源码就正常了:
kotlin
public fun <T> Array<out T>.toMutableList(): MutableList<T> {
return ArrayList(this.asCollection())
}
这就是这个问题的全貌了,用了这么多年 Kotlin,感觉 Kotlin 中这样的坑并不多,这算是为数不多的一次偷袭。