类型擦除
在 Kotlin
中,类型擦除是一个与泛型相关的重要概念,它和 Java
的类型擦除机制类似,因为 Kotlin
在 JVM
平台上运行时,很多泛型特性是基于 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
}
在上述代码中,stringList
和 intList
虽然在编译时具有不同的泛型类型参数(String
和 Int
),但在运行时,它们的实际类型都是 java.util.Arrays$ArrayList
,泛型类型参数被擦除了。
为什么要类型擦除
Java
在引入泛型(Java 5
)之前,已经存在了大量的非泛型代码。为了确保这些旧代码能够继续运行,Java
需要在不破坏现有代码的基础上引入泛型。
在 Java 5
之前,集合类(如 ArrayList
、HashMap
)都是非泛型的,使用 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
检查和类型转换,并且在操作时可以省略泛型类型参数。