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

相关推荐
李艺为3 小时前
Fake Device Test作假屏幕分辨率分析
android·java
zh_xuan3 小时前
github远程library仓库升级
android·github
峥嵘life4 小时前
Android蓝牙停用绝对音量原理
android
小书房4 小时前
Kotlin的内联函数
java·开发语言·kotlin·inline·内联函数
czlczl200209255 小时前
IN和BETWEEN在索引效能的区别
android·adb
Volunteer Technology5 小时前
ES高级搜索功能
android·大数据·elasticsearch
北京自在科技5 小时前
Find Hub App 小更新
android·ios·安卓·findmy·airtag
lbb 小魔仙6 小时前
2026远程办公软件夏季深度横测:ToDesk、向日葵、网易UU远程全面对比,远控白皮书
android·服务器·网络协议·tcp/ip·postgresql
coding_fei6 小时前
AudioServer初始化过程
android
brucelee1866 小时前
Docker 运行 Android 模拟器
android·docker·容器