Java 和 Kotlin 混编导致的 bug

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 中这样的坑并不多,这算是为数不多的一次偷袭。

相关推荐
盒子里的猫有猫版38 分钟前
kotlin学习——空安全
kotlin
Mr YiRan43 分钟前
Android Gradle多渠道打包
android
IvanCodes2 小时前
MySQL 视图
android·数据库·sql·mysql·oracle
好学人2 小时前
Android动画系统全面解析
android
leverge20092 小时前
android studio 运行java main报错
android·ide·android studio
RichardLai882 小时前
Flutter 环境搭建
android·flutter
思想觉悟2 小时前
ubuntu编译android12源码
android·ubuntu·源码
好学人3 小时前
Android自定义控件事件传递机制
android
V少年3 小时前
深入浅出 C++ 标准库
android