JVM 的泛型一般是通过类型擦除实现的,就是说泛型的类型实参在运行时不保留。
和 Java 一样,Kotlin 的泛型在运行时也被擦除了,但是 Kotlin 可以通过将函数声明为 inline 来解决这个问题。Kotlin 可以声明一个 inline 函数,使用实化 reified 使其类型实参不被擦除。
类型检查与转换
在运行时,List 的类型实参 String 被擦除了,只能知道它是一个 List,不能知道它是否声明为一个字符串列表。
list1 和 list2 在运行时每个都是 List,不能看出它们是否声明为字符串或者整数列表。
fun erase() {
val list1: List<String> = listOf("a", "b");
val list2: List<Int> = listOf(1, 2, 3)
}
类型检查
因为类型实参被擦除,不能检查带类型实参的泛型。
下面的代码不会编译,因为运行时 List 的类型实参被擦除。
fun <T> checkInstance(value: List<T>) {
// 编译器报错Cannot check for instance of erased type: List<String>
if (value is List<String>) {
println("is string list.")
}
}
下面的代码可以编译过,用来检查一个值是否是列表,而不是 set 或者其他对象。* 是星号投影,表示未知类型实参的泛型类型,相当于 Java 的 List<?>。
fun <T> checkListInstance(value: List<T>) {
if (value is List<*>) {
println("is a list with any type")
}
}
类型转换
可以使用 as 或者 as? 转换泛型类型。
下面的代码可以编译,只是报警告:Unchecked cast: Collection<*> to List。未经过检查的类型转换。
fun printSum(c: Collection<*>) {
val intList = c as? List<Int> ?: throw IllegalArgumentException("List is expected");
println(intList.sum())
}
当 c 可以转换成 List 时,打印列表元素之和,否则抛出 IllegalArgumentException。
传入 List 时,正常返回列表元素的和。
fun main() {
// 6
printSum(listOf(1, 2, 3))
}
传入 Set 时,擦除后的泛型类型 Set 无法转换为 List,抛出 IllegalArgumentException。
fun main() {
// Exception in thread "main" java.lang.IllegalArgumentException: List is expected
printSum(setOf(1, 2, 3))
}
传入 List 时,能转换成功,但是 sum() 报错,抛出 ClassCastException,因为 Kotin 试图将元素转化为 Int 然后 sum,但是 String 无法转换为 Int。
fun main() {
// Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Number
printSum(listOf("a", "b", "c"))
}
已知类型实参的转换
Kotlin 对于编译期间已知的类型信息,做 is 检查是允许的。
下面的代码编译器不会报错也不会报警告,因为参数的类型实参是已知的,为 Int。
fun printSumInt(c: Collection<Int>) {
if (c is List<Int>) {
println(c.sum())
}
}
fun main() {
// 6
printSumInt(listOf(1, 2, 3))
}
带实化类型的函数
泛型函数的类型参数在运行时也会被擦除。下面的方法无法编译,因为类型参数被擦除,运行时不知道类型参数 T 的具体类型,无法检查类型参数 T 的类型。
// Cannot check for instance of erased type: T
// Make type parameter reified and function inline
fun <T> isA(value: Any) = value is T
但是编译器也给出了提示 Make type parameter reified and function inline,将类型参数实化并且变为内联函数。
inline 和 reified
下面的方法可以编译通过,正常运行。区别在于函数变为内联函数 inline,类型参数变为实化参数 reified。
inline fun <reified T> isA(value: Any) = value is T
fun printIsA() {
// true
println(isA<String>("abc"))
// false
println(isA<String>(123))
}
filterIsInstance 函数
filterIsInstance 是标准库中的一个内联实化函数。它会过滤列表中所有类型为指定类型参数的元素。这里是 String 类型。
fun filterIsInstance() {
val items = listOf("one", 2, "three")
// [one, three]
println(items.filterIsInstance<String>())
}
查看 filterIsInstance 的源码实现,可以看出它是一个内联函数 inline fun,同时泛型类型参数 R 被实化 reified,因此不会擦除类型参数。
因此在函数内部可以使用 element is R 类型检查,只有类型为 R 的元素才会被加入到列表。
public inline fun <reified R> Iterable<*>.filterIsInstance(): List<@kotlin.internal.NoInfer R> {
return filterIsInstanceTo(ArrayList<R>())
}
public inline fun <reified R, C : MutableCollection<in R>> Iterable<*>.filterIsInstanceTo(destination: C): C {
for (element in this) if (element is R) destination.add(element)
return destination
}
类型实化后 Kotlin 生成的字节码和以下代码类似。类型实参 String 替换了类型参数 T,因此可以使用 element is String 类型检查。
fun afterReified() {
val items = listOf("one", 2, "three")
val destination = ArrayList<String>()
for (element in items) {
if (element is String) {
destination.add(element)
}
}
// [one, three]
println(destination)
}
实化类型代替类引用
实化类型的一个常用场景是封装 Java.lang.Class 类的引用。
loadService 函数
下面的代码使用 ServiceLoader 加载 Service::class.java,返回一个 serviceImpl 实现类。Service::class.java 是 Kotlin 的 Service 类在 Java 中的对应类。
loadService 是使用内类函数和实化类型参数的改造函数。可以看出 loadService 的语法跟简单,只需要传入类型实参到 即可。
fun reifiedClass() {
val serviceImpl = ServiceLoader.load(Service::class.java)
// reified
val serviceImpl2 = loadService<Service>()
}
inline fun <reified T> loadService() {
ServiceLoader.load(T::class.java)
}
interface Service {
fun work()
}
简化 startActivity
在 Android 开发中使用 reified 实化类引用的场景是启动 Activity。
下面的代码给 Intent 传入了 SampleActivity::class.java,然后启动 SampleActivity。
fun startActivity(context: Context) {
val intent = Intent(context, SampleActivity::class.java)
context.startActivity(intent)
}
使用 reified 实化需要启动的 Activity 类,并将它作为 Context 接收者的扩展函数。
inline fun <reified T> Context.startActivity() {
val intent = Intent(this, T::class.java)
startActivity(intent)
}
可以进一步封装,指定类型参数的泛型上界为 Activity,可选参数 Bundle。
inline fun <reified T : Activity> Activity.startActivity(bundle: Bundle? = null) {
val intent = Intent(this, T::class.java)
bundle?.let {
intent.putExtras(it)
}
startActivity(intent)
}
简化后只需要调用 startActivity(),指定类型实参为 SampleActivity。
startActivity<SampleActivity>()
实化类型的限制
实化类型参数很方便,但是它也有一些限制。
可以使用实化类型参数的几个方面:
- 类型检查和类型转换 is, !is, as, as?
- Kotlin 反射 API,::class
- 获取 Java.lang.Class,::class.java
- 作为类型参数用来调用其他函数
不能使用实化类型参数的几个方面:
- 创建类型参数指定的类实例
- 调用类型参数的伴生对象的方法
- 调用实化类型参数的函数时,使用非实化类型参数作为类型参数
- 将类、属性、或者非内联函数的类型参数变为实化类型