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

相关推荐
simplepeng3 小时前
Room 3.0 KMP Alpha-01
android·kotlin·android jetpack
Lei活在当下3 小时前
Windows 下 Codex 高效工作流最佳实践
android·openai·ai编程
fatiaozhang95273 小时前
基于slimBOXtv 9.19.0 v4(通刷晶晨S905L3A/L3AB芯片)ATV-安卓9-完美版线刷固件包
android·电视盒子·刷机固件·机顶盒刷机·晶晨s905l3ab·晶晨s905l3a
私房菜4 小时前
Selinux 及在Android 的使用详解
android·selinux·sepolicy
一只特立独行的Yang4 小时前
Android中的系统级共享库
android
两个人的幸福online5 小时前
php开发者 需要 协程吗
android·开发语言·php
修炼者6 小时前
WindowManager(WMS)构建全局悬浮窗
android
xiaoshiquan12067 小时前
Android Studio里,SDK Manager显示不全问题
android·ide·android studio
Lstone73647 小时前
Bitmap深入分析(一)
android
一起搞IT吧8 小时前
Android功耗系列专题理论之十四:Sensor功耗问题分析方法
android·c++·智能手机·性能优化