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

相关推荐
doublelixin2 小时前
AOSP (Android11) 集成Google GMS三件套
android
xzkyd outpaper5 小时前
onSaveInstanceState() 和 ViewModel 在数据保存能力差异
android·计算机八股
梓仁沐白6 小时前
【Kotlin】协程
开发语言·python·kotlin
CYRUS STUDIO6 小时前
FART 脱壳某大厂 App + CodeItem 修复 dex + 反编译还原源码
android·安全·逆向·app加固·fart·脱壳
WAsbry6 小时前
现代 Android 开发自定义主题实战指南
android·kotlin·material design
xzkyd outpaper7 小时前
Android动态广播注册收发原理
android·计算机八股
唐墨1237 小时前
android与Qt类比
android·开发语言·qt
林林要一直努力8 小时前
Android Studio 向模拟器手机添加照片、视频、音乐
android·智能手机·android studio
AD钙奶-lalala8 小时前
Mac版本Android Studio配置LeetCode插件
android·ide·android studio
梓仁沐白9 小时前
【Kotlin】注解&反射&扩展
开发语言·python·kotlin