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

相关推荐
2501_9151063221 分钟前
iOS 性能优化这件事,结合多工具分析运行期性能问题
android·ios·性能优化·小程序·uni-app·cocoa·iphone
千里马学框架27 分钟前
如何使用豆包手机的READ_FRAME_BUFFER权限截图密码画面
android·智能手机·framework·安卓framework开发·权限·截图·secure
游戏开发爱好者830 分钟前
App Store 上架流程,结合多工具协作
android·ios·小程序·https·uni-app·iphone·webview
阿道夫小狮子31 分钟前
android 音频抢占问题
android·音视频
撩得Android一次心动43 分钟前
Android 四大组件——Service(服务)【基础篇1】
android·服务·四大组件
峥嵘life1 小时前
Android16 EDLA 认证测试BTS过程介绍
android·java·linux
茶憶2 小时前
UniApp 安卓端实现文件的生成,写入,获取文件大小以及压缩功能
android·javascript·vue.js·uni-app
2501_915921432 小时前
uni-app 的 iOS 打包与上架流程,多工具协作
android·ios·小程序·uni-app·cocoa·iphone·webview
Lei活在当下9 小时前
【Perfetto从入门到精通】4.使用 heapprofd 工具采样追踪 Java/Native 内存分配
android·性能优化·架构
alexhilton10 小时前
学会在Jetpack Compose中加载Lottie动画资源
android·kotlin·android jetpack