Kotlin进阶知识(十)——运行时的泛型:擦除和实化类型参数

261 阅读3分钟

引言:JVM上的泛型一般是通过类型擦除实现的,就是说泛型类实例的类型实参在运行时是不保留的

一、运行时的泛型:类型检查和转换

和Java一样,Kotlin的泛型在运行时也被擦除了。这意味着泛型实例不会携带用于创建它的类型实参的信息。

检查一个值是否在列表,而不是set或者其他对象的方式:可以使用特殊的星号投影语法来做这种检查:

if (value is List<*>) { ... }

实际上,泛型类型拥有的每个类型形参都需要一个***[拥有未知类型实参的泛型类型**(或者类比于Java的List<?>)]。

  • 对泛型类型做泛型转换
fun printSum(c: Collection<*>) {
    // 这里会有警告。Unchecked cast: List<*> to List<Int>
    val intList = c as? List<Int>
        ?: throw IllegalArgumentException("List is expected")

    println(intList.sum())
}

fun printSumTest() {
    // 一切都符合预期要求
    printSum(listOf(1, 2, 3))

    // Set不是列表,所以抛出了异常
    printSum(setOf(1, 2, 3))

    // 类型转换成功,但后面抛出了另外的异常
    printSum(listOf("a", "b", "c"))
}

// 输出结果
6

java.lang.IllegalArgumentException: List is expected

java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Number

因为编译器没有办法判断实参listOf("a", "b", "c")是不是一个List,因而类型转换会成功,但是iniList.sum()调用时把StringNumber用的尝试会导致运行时的ClassCastException

注意:Kotlin编译器是足够智能的,在编译期它已经知道相应的类型信息时,is检查是允许的

  • 对已知类型实参做类型转换
fun printSum(c: Collection<Int>) {
    // 这次检查是合法的
    if (c is List<Int>) {
        println(c.sum())
    }
}

>>> printSum(listOf(1, 2, 3))
6

上述例子中,c是否拥有类型List<Int>的检查是可行的,因为在编译器就确定了集合(不管它是列表还是其他类型的集合)包含的是整型数字

二、声明带实化类型参数的函数

本小节展示inline函数的另一种场景:它们的类型参数可以被实化,使用到的关键字——reified

// 现在代码可以编译
fun <T> isA(value: Any) = value is T
Error: Cannot check for instance of erased type: T

// 现在代码可以编译
inline fun <reified T> isA(value: Any) = value is T

fun isATest() {
    println(isA<String>("abc"))

    println(isA<String>(123))
}

// 输出结果
true
false
  • filterIsInstance的简化实现
// “reified”声明了类型参数不会再运行时被擦除
public 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
}

为什么实化只对内联函数有效 编译器把实现内联函数的字节码插入每一次调用发生的地方。因为生成的字节码引用了具体类,而不是类型参数,它不会被运行时发生的类型参数擦除影响。

注意:reified类型参数的inline函数 不能Java代码中调用。普通的内联函数可以像常规函数那样在Java中调用——它们可以被调用而不能被内联。

三、实化类型参数的限制

具体来说,可以按下面的方式**使用实化类型参数**:

  • 用在类型检查和类型转换中(is、!is、as、as?
  • 使用Kotlin反射API::class
  • 获取相应的**java.lang.Class**(::class.java
  • 作为调用其他函数的类型实参

**不能**做下面这些事情:

  • 创建指定为类型参数的类的实例
  • 调用类型参数的伴生对象的方法
  • 调用带实化类型参数的时候使用非实化类型形参为类型实参
  • 类、属性或者非内联类型参数标记成**reified**