Kotlin 2.1.0 入门教程(二十五)类型擦除

类型擦除

Kotlin 中,类型擦除是一个与泛型相关的重要概念,它和 Java 的类型擦除机制类似,因为 KotlinJVM 平台上运行时,很多泛型特性是基于 Java 的泛型实现的。

什么是类型擦除

类型擦除是指在编译时,泛型类型的具体类型信息会被擦除,只保留原始类型。也就是说,在运行时,泛型类型的具体类型参数是不可用的。例如,List<String>List<Int> 在运行时都会被擦除为 List

kotlin 复制代码
fun main() {
    val stringList: List<String> = listOf("apple", "banana")
    val intList: List<Int> = listOf(1, 2)

    println(stringList.javaClass) // class java.util.Arrays$ArrayList
    println(intList.javaClass)    // class java.util.Arrays$ArrayList
}

在上述代码中,stringListintList 虽然在编译时具有不同的泛型类型参数(StringInt),但在运行时,它们的实际类型都是 java.util.Arrays$ArrayList,泛型类型参数被擦除了。

为什么要类型擦除

Java 在引入泛型(Java 5)之前,已经存在了大量的非泛型代码。为了确保这些旧代码能够继续运行,Java 需要在不破坏现有代码的基础上引入泛型。

Java 5 之前,集合类(如 ArrayListHashMap)都是非泛型的,使用 Object 类型来存储元素。

java 复制代码
List list = new ArrayList();
list.add("Hello");
String str = (String)list.get(0); // 需要强制类型转换。

引入泛型后,Java 需要确保这些旧代码仍然能够运行。通过类型擦除,泛型类型在编译时会被替换为 Object 或指定的边界类型(如 T extends Number),从而与非泛型代码兼容。

java 复制代码
List<String> list = new ArrayList<>();
list.add("Hello");
String str = list.get(0); // 不需要强制类型转换。

在编译后会被擦除为:

java 复制代码
List list = new ArrayList();
list.add("Hello");
String str = (String)list.get(0); // 编译器自动插入强制类型转换。

这样,泛型代码和非泛型代码可以在同一个 JVM 上运行。

类型擦除简化了 Java 泛型的实现,避免了在运行时维护复杂的类型信息。

如果 Java 在运行时保留泛型类型信息,JVM 需要为每个泛型类型生成新的类,这会增加内存开销和运行时的复杂性。通过类型擦除,泛型类型在运行时都被视为原始类型(如 Object),从而减少了运行时的开销。

如果不使用类型擦除,Java 需要为每个泛型类型组合生成一个新的类。例如,List<String>List<Integer> 会被视为不同的类,这会导致类的数量急剧增加(类爆炸问题)。类型擦除避免了这一问题,因为它们在运行时都是 List

类型擦除的局限性

由于类型擦除,运行时无法直接检查一个 List 是否是 List<String>

java 复制代码
List<String> list = new ArrayList<>();
if (list instanceof List<String>) { // 编译错误。
}

由于类型擦除,无法直接使用泛型类型参数创建实例。

java 复制代码
public <T> void createInstance() {
    T instance = new T(); // 编译错误。
}

由于类型擦除,无法直接创建泛型数组。

java 复制代码
List<String>[] array = new List<String>[10]; // 编译错误。

类型擦除的局限性

由于类型擦除,在运行时无法直接获取泛型类型参数。

kotlin 复制代码
fun <T> printType(list: List<T>) {
    // 无法获取 T 的具体类型。
    println(T::class) // 编译错误。
}

由于类型擦除,不能使用泛型类型参数来创建对象。

kotlin 复制代码
fun <T> createInstance(): T {
    return T() // 编译错误。
}

由于类型擦除,不能使用泛型类型参数进行类型检查。

kotlin 复制代码
fun <T> checkType(list: List<T>) {
    if (list is List<String>) { // 编译错误。
        println("It's a list of strings")
    }
}

类型擦除

Kotlin 对泛型声明所执行的类型安全检查是在编译时完成的。在运行时,泛型类型的实例不会保留关于其实际类型参数的任何信息。这种类型信息的丢失被称为类型擦除。例如,Foo<Bar>Foo<Baz?> 的实例在运行时都会被擦除为 Foo<*>

由于存在类型擦除,在运行时没有通用的方法来检查一个泛型类型的实例是否是使用特定的类型参数创建的,并且编译器禁止进行像 ints is List<Int> 或者 list is T 这样的 is 检查。不过,你可以针对 * 投影类型来检查实例:

kotlin 复制代码
fun main() {
    val ints: List<Int> = listOf(1, 2, 3)
    val strings: List<String> = listOf("a", "b", "c")

    checkIfList(ints)
    checkIfList(strings)
}

fun checkIfList(something: Any) {
    if (something is List<*>) {
        // 如果 something 是一个列表,遍历并打印列表中的每个元素。
        // 列表中的元素被视为 Any? 类型。
        something.forEach { println(it) }
    }
}

checkIfList 函数中,我们使用了 * 投影类型 List<*> 来进行 is 检查。* 投影类型表示我们不关心列表中元素的具体类型,只关心它是否是一个列表。由于在运行时可以确定对象是否是一个列表,所以这种检查是被允许的。

something 被判断为 List<*> 类型后,列表中的元素会被视为 Any? 类型。这是因为我们不知道列表中元素的具体类型,所以只能将其当作最通用的类型 Any? 来处理。在 forEach 循环中,我们可以对这些元素执行一些通用的操作,比如打印它们。

综上所述,由于类型擦除,我们无法在运行时对泛型类型的具体类型参数进行检查,但可以使用 * 投影类型来进行更宽泛的类型检查。

Kotlin 里,泛型类型在编译时会进行静态类型检查,以此保证代码的类型安全性。不过在运行时,由于类型擦除机制,泛型类型的具体类型参数信息会被抹去。然而,当我们在编译时已经对泛型实例的类型参数做了检查,就能够在运行时对类型的非泛型部分开展 is 检查或者类型转换。

kotlin 复制代码
fun handleStrings(list: MutableList<String>) {
    if (list is ArrayList) {
        // list 被自动转换为 ArrayList<String> 类型。
        println("The list is an instance of ArrayList.")
    }
}

fun main() {
    val stringList: MutableList<String> = ArrayList()
    stringList.add("hello")
    stringList.add("world")
    handleStrings(stringList)
}

handleStrings 函数中,参数 list 被声明为 MutableList<String>,这意味着在编译时,编译器会确保传入的列表元素类型是 String

if (list is ArrayList) 这一行,我们进行了一个 is 检查,不过这里省略了泛型类型参数 <String>。这是因为类型擦除使得在运行时无法获取泛型的具体类型参数,所以检查的重点是对象是否为 ArrayList 这个类的实例。

一旦 is 检查通过,Kotlin 会进行智能类型转换,把 list 自动转换为 ArrayList<String> 类型。这表明在 if 语句块里,我们可以把 list 当作 ArrayList<String> 来使用,编译器清楚其元素类型是 String

kotlin 复制代码
fun main() {
    val stringList: MutableList<String> = ArrayList()
    stringList.add("apple")
    stringList.add("banana")

    val arrayList = stringList as ArrayList
    println("The size of the ArrayList is: ${arrayList.size}")
}

stringList as ArrayList 这种写法在类型转换时省略了泛型类型参数。因为类型擦除的存在,在运行时不考虑泛型类型参数,这里只是把 stringList 转换为 ArrayList 类型。同样,由于编译时已经确定 stringList 的元素类型是 String,所以转换后的 arrayList 实际上是 ArrayList<String> 类型,我们可以按照 ArrayList<String> 来使用它。

综上所述,当编译时已经对泛型实例的类型参数完成检查后,在运行时可以对类型的非泛型部分进行 is 检查和类型转换,并且在操作时可以省略泛型类型参数。

相关推荐
小蜜蜂嗡嗡1 小时前
Android Studio flutter项目运行、打包时间太长
android·flutter·android studio
aqi001 小时前
FFmpeg开发笔记(七十一)使用国产的QPlayer2实现双播放器观看视频
android·ffmpeg·音视频·流媒体
zhangphil3 小时前
Android理解onTrimMemory中ComponentCallbacks2的内存警戒水位线值
android
你过来啊你3 小时前
Android View的绘制原理详解
android
移动开发者1号6 小时前
使用 Android App Bundle 极致压缩应用体积
android·kotlin
移动开发者1号6 小时前
构建高可用线上性能监控体系:从原理到实战
android·kotlin
ii_best11 小时前
按键精灵支持安卓14、15系统,兼容64位环境开发辅助工具
android
美狐美颜sdk11 小时前
跨平台直播美颜SDK集成实录:Android/iOS如何适配贴纸功能
android·人工智能·ios·架构·音视频·美颜sdk·第三方美颜sdk
恋猫de小郭15 小时前
Meta 宣布加入 Kotlin 基金会,将为 Kotlin 和 Android 生态提供全新支持
android·开发语言·ios·kotlin
aqi0016 小时前
FFmpeg开发笔记(七十七)Android的开源音视频剪辑框架RxFFmpeg
android·ffmpeg·音视频·流媒体