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 检查和类型转换,并且在操作时可以省略泛型类型参数。

相关推荐
hmywillstronger2 分钟前
【Excel】【VBA】根据内容调整打印区域
android·excel
coooliang42 分钟前
【Android】ViewPager的使用
android
simplepeng11 小时前
我的天,我真是和androidx的字体加载杠上了
android
小猫猫猫◍˃ᵕ˂◍13 小时前
备忘录模式:快速恢复原始数据
android·java·备忘录模式
CYRUS_STUDIO14 小时前
使用 AndroidNativeEmu 调用 JNI 函数
android·逆向·汇编语言
梦否14 小时前
【Android】类加载器&热修复-随记
android
徒步青云15 小时前
Java内存模型
android
今阳15 小时前
鸿蒙开发笔记-6-装饰器之@Require装饰器,@Reusable装饰器
android·app·harmonyos
-优势在我20 小时前
Android TabLayout 实现随意控制item之间的间距
android·java·ui