类型擦除
泛型在编译后类型会被擦除,所以在程序运行时候我们是不知道泛型的信息的。这里不知道泛型的信息听起来可能会有点抽象,我举个例子,在声明泛型List< String >的时候,编译后它的String类型会擦除,运行时候它仅仅是一个List, 我们并不能知道这个List里面放的是string对象.
例如:
val list1: List<String> = listOf("a", "b")
val list2: List<Int> = listOf(1, 2, 3)
在编译后实际运行中
它们仅仅是一个List.
这样的类型擦除会造成一个问题,就是我们在写代码时候不能确定泛型是哪个泛型. 例如:
>>> if (value is List<String>) { ... }
ERROR: Cannot check for instance of erased type
因为类型擦除的原因,导致运行时候不能确认它是一个String的List.
Kotlin不允许使用没有指定类型参数的泛型。那我们怎么去检查一个对象是否是一个List呢,这里可以用星投射<*>, 例如
if (value is List<*>) { ... }
不过这里你仍然可以使用as和as?进行强制类型转换, 即使这种强转有对应的基础类型,但是类型参数不对也不会失败,因为当运行时强转这个类型参数是未知的,这个时候它只是给一个警告“unchecked cast”。
fun printSum(c: Collection<*>) {
val intList = c as? List<Int>
?: throw IllegalArgumentException("List is expected")
println(intList.sum())
}
fun main(args: Array<String>) {
printSum(listOf(1, 2, 3))
}
这段代码是能够编译通过的,运行时候我们按照下面测试就会出现对应的异常
>>> printSum(setOf(1, 2, 3))
IllegalArgumentException: List is expected
>>> printSum(listOf("a", "b", "c"))
ClassCastException: String cannot be cast to Number
这里我们来分析一下printSum(listOf("a", "b", "c"))为什么会抛出ClassCastException
首先我们之前提到了,编译后会进行泛型擦除,那么这里虽然是一个string list强转List< Int >也不会有什么问题,接下来调用intList.sum()就会抛出ClassCastException了,因为string 不能 强转为int.
那如果我改下printSum实现是可以防止这种现象发生的
fun printSum(c: Collection<Int>) {
if (c is List<Int>) {
println(c.sum())
}
}
fun main(args: Array<String>) {
printSum(listOf(1, 2, 3))
}
在这里我们提供足够的参数类型,保证编译以前确保对应的泛型,使它能够调用is确定是List< Int >类型.
使用inline具像化泛型
上面提到过泛型编译后会进行类型擦除,这导致我们在函数里面不能够确定泛型参数的类型,例如:
>>> fun <T> isA(value: Any) = value is T
Error: Cannot check for instance of erased type: T
这种场景想要实现,我们可以通过inline函数具像化参数.
inline fun <reified T> isA(value: Any) = value is T
fun main(args: Array<String>) {
println(isA<String>("abc"))
println(isA<String>(123))
}
之所以上面的写法能够成立,因为inline方法在编译时候会被替换成实际的执行代码,准确来讲它就不是一个函数,这样在编译时候是能够确认reified T的类型的. 下面再举个更实际的例子,更加容易理解inline具象化泛型的神奇之处. kotlin集合的扩展函数有一个filterIsInstance,用来过滤集合中的某个类型。
fun main(args: Array<String>) {
val items = listOf("one", 2, "three")
println(items.filterIsInstance<String>())
}
如果没有inline具象化泛型,这个功能是做不了的,因为类型擦除后,在filterIsInstance是判断不了泛型到底是什么类型的,不过利用inline具象化我们看看filterIsInstance的实现
inline fun <reified T>
Iterable<*>.filterIsInstance(): List<T> {
val destination = mutableListOf<T>()
for (element in this) {
if (element is T) {
destination.add(element)
}
}
return destination
}
这里我们可以判断参数是否是泛型T了。
为什么使用inline后可以具象化泛型了?
因为使用inline标记的函数,它和普通函数是有区别的,我们知道kotlin函数实际上也是一个类,每个函数会编译成一个对象,但是inline函数在编译的时候会直接编译成字节码,不会生成函数对象,这样实际上就不存在函数使用泛型参数的情况了。