公众号「稀有猿诉」 原文链接 Deep Dive into Kotlin Generics Runtime
通过前面的学习,对Kotlin的泛型已经有了比较全面的了解了,泛型的目的是让通用的代码更加的类型安全。现在我们离写出类型安全的泛型代码还差最后一块拼图,那就是泛型的类型擦除,今天就来深入地学习一下运行时的泛型,彻底的弄懂类型擦除的前因后果,并学会如何在运行时做类型检查和类型转换,以期完成拼图掌握泛型,写出类型安全的通用代码。
关于泛型话题的一系列文章:
- 这回就好好聊聊Kotlin的泛型
- 深入浅出Java泛型
- 再次深入解析Kotlin泛型
- 深入研究Kotlin运行时的泛型
泛型类型擦除(Type erasure)
泛型的类型安全性(包括类型检查type check,和类型转换type casting)都是由编译器在编译时做的,为了保持在JVM上的兼容性,编译器在保障完类型安全性后会对泛型类型进行擦除(Type erasure)。在运行时泛型类型的实例并不包含其类型信息,也就是说它不知道具体的类型参数,比如Foo<Bar>和Foo<Baz?>都被擦除成了Foo<*>,在虚拟机(JVM)来看,它们的类型是一样的。
因为泛型Foo<T>的类型参数T会被擦除(erased),所以与类型参数相关的类型操作(类型检查is T和类型转换as T)都是不允许的。
可行的类型检查和转换
虽然类型参数会被擦除,但并不是说对泛型完全不能进行类型操作。
星号类型操作
因为所有泛型会被擦除成为星号无界通配Foo<*>,它相当于Foo<Any?>,是所有Foo泛型的基类,类型参数Any?是根基类,所以可以进行类型检查和类型转换:
if (something is List<*>) {
something.forEach { println(it) } // 元素被视为Any?类型
}
针对星号通配做类型操作,类型参数会被视为Any?。但其实这种类型操作没有任何意义,毕竟Any是根基类,任何类当成Any都是没有问题的。
完全已知具体的类型参数时
另外一种情况就是,整个方法的上下文中已经完全知道了具体的类型参数时,不涉及泛型类型时,也是可以进行类型操作的,说的比较绕,我们来看一个🌰:
fun handleStrings(list: MutableList<String) {
if (list is ArrayList) {
// list is smart-cast to ArrayList<String>
}
}
这个方法并不涉及泛型类型,已经知道了具体的类型参数是String,所以类型操作也是可行的,因为编译器知道具体的类型,能对类型进行检查 保证是类型安全的。并且因为具体类型参数String可以推断出来,所以<String>是可以省略的。
未检查的转换
当编译器能推断出具体的类型时,进行类型转换就是安全的,这就是被检查的转型(checked cast),如上面的🌰。
如果无法推断出类型时,比如涉及泛型类型T时,因为类型会被擦除,编译器不知道具体的类型,这时as T或者as List<T>都是不安全的,编译器会报错,这就是未检查转型(unchecked cast)。
但如果能确信是类型转换是安全的,可以用注解@Suppress("UNCHECKED_CAST")来忽略。
用关键reified修饰inline泛型函数
要想能够对泛型类型参数T做类型操作,只能是在用关键字reified修饰了的inline泛型函数,在这种函数体内可以对泛型类型参数T做类型操作,🌰如:
inline fun <reified A, reified B> Pair<*, *>.asPairOf(): Pair<A, B>? {
if (first !is A || second !is B) return null
return first as A to second as B
}
val somePair: Pair<Any?, Any?> = "items" to listOf(1, 2, 3)
val stringToSomething = somePair.asPairOf<String, Any>()
val stringToInt = somePair.asPairOf<String, Int>()
需要注意的是关键字reified能够让针对类型参数T的操作得到编译器的检查,保证安全,是允许的。但是对于泛型仍是不允许的,🌰如:
inline fun <reified T> List<*>.asListOfType(): List<T>? =
if (all { it is T })
@Suppress("UNCHECKED_CAST")
this as List<T> else
null
这个inline泛型函数用关键字reified修饰了,因此针对类型参数T是允许类型检查类型转换,如第2行是允许的。但泛型仍是不合法,如第4行,这时可以用上一小节提到的注解@Suppress("UNCHECKED_CAST")来忽略未检查类型转换。
inline和reified的原理
对于一些泛型工厂方法,就非常适合使用inline和reified,以保证转换为类型参数(因为工厂方法最终肯定要as T)是允许的且是安全的:
inline fun <reified T> logger(): Logger = LoggerFactory.getLogger(T::class.java)
class User {
private val log = logger<User>()
// ...
}
关键字reified其实也没有什么神秘的,因为这是inline函数,这种函数是会把函数体嵌入到任何调用它的地方(call site),而每个调用泛型函数的地方必然会有明确的具体类型参数,那么编译器就知道了具体的类型能保证类型安全(checked cast)。上面的工厂方法在调用时就会大概变成酱紫:
class User {
private val log = LoggerFactory.getLogger(User.class.java)
}
这时其实在函数体内已经知道了具体的类型参数User,编译器能够进行类型检查,所以是安全的。
总结
本文深入的讨论一下运行时泛型的一些特性,泛型类型在运行时会被擦除,无法做泛型相关的类型操作,因为编译器无法保证其类型安全。例外就是在用reified修饰的inline函数中可以对类型参数T做类型操作,但泛型类型(带尖括号的<T>)仍是会被擦除,可以用注解@Suppress("UNCHECKED_CAST")来忽略unchecked cast。
参考资料
- Type erasure
- 6. Generics at Runtime
- How to Convert a Type-Erased List to an Array in Kotlin
- Discussion about Type Erasure
- How does erasure work in Kotlin?
- Reified Generics in Kotlin
- Type erasure and reified in Kotlin
欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!
原创不易,「打赏」,「点赞」,「在看」,「收藏」,「分享」 总要有一个吧!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。