Kotlin Reified 实化类型参数

1. 泛型类型擦除

我们都知道 JVM 中的泛型一般是通过类型擦除实现的,也就是说泛型类实例的类型实参在编译时被擦除,在运行时是不会被保留的

基于这样实现的做法是有历史原因的,最大的原因之一是为了兼容JDK1.5之前的版本,当然泛型类型擦除也是有好处的,在运行时丢弃了一些类型实参的信息,对于内存占用也会减少很多

正因为泛型类型擦除原因在业界 Java 的泛型又称伪泛型。因为编译后所有泛型的类型实参类型都会被替换为 Object 类型或者泛型类型形参指定上界约束类的类型

例如:List<Float>、List<String>、List<Student>在 JVM 运行时Float、String、Student都被替换成Object类型,如果是泛型定义是List<T extends Student>那么运行时T被替换成Student类型,具体可以通过反射Erasure类可看出。

虽然 Kotlin 没有和 Java 一样需要兼容旧版本的历史原因,但是由于 Kotlin 编译器编译后出来的 class 也是要运行在和 Java 相同的 JVM 上的,JVM的泛型一般都是通过泛型擦除,所以 Kotlin 始终还是迈不过泛型擦除的坎。

但是 Kotlin 是一门有追求的语言,不想再被 C# 那样喷 Java 说什么泛型集合连自己的类型实参都不知道,所以 Kotlin 借助 inline 内联函数玩了个小魔法。

2. 泛型擦除会带来什么影响?

泛型擦除会带来什么影响,这里以 Kotlin 举例,因为 Java 遇到的问题,Kotlin 同样需要面对。看个例子:

fun main(args: Array<String>) {
    val list1: List<Int> = listOf(1,2,3,4)
    val list2: List<String> = listOf("a","b","c","d")
    println(list1)
    println(list2)
}

上面两个集合分别存储了 Int 类型的元素和 String 类型的元素,但是在编译后的 class 文件中的他们被替换成了 List 原生类型,一起来看下反编译后的 Java 代码:

@Metadata(
   mv = {1, 1, 11},
   bv = {1, 0, 2},
   k = 2,
   d1 = {"u0000u0014nu0000nu0002u0010u0002nu0000nu0002u0010u0011nu0002u0010u000enu0002bu0002u001au0019u0010u0000u001au00020u00012fu0010u0002u001abu0012u0004u0012u00020u00040u0003¢u0006u0002u0010u0005¨u0006u0006"},
   d2 = {"main", "", "args", "", "", "([Ljava/lang/String;)V", "Lambda_main"}
)
public final class GenericKtKt {
   public static final void main(@NotNull String[] args) {
      Intrinsics.checkParameterIsNotNull(args, "args");
      List list1 = CollectionsKt.listOf(new Integer[]{1, 2, 3, 4});//List原生类型
      List list2 = CollectionsKt.listOf(new String[]{"a", "b", "c", "d"});//List原生类型
      System.out.println(list1);
      System.out.println(list2);
   }
}

我们看到编译后 listOf 函数接收的是 Object 类型,不再是具体的 String 和 Int 类型了。

图片描述

2.1 类型检查问题

Kotlin 中的 is 类型检查,一般情况不能检测类型实参中的类型(注意是一般情况,后面特殊情况会细讲),类似下面:

if(value is List<String>){...}//一般情况下这样的代码不会被编译通过

分析:尽管我们在运行时能够确定 value 是一个 List 集合,但是却无法获得该集合中存储的是哪种类型的数据元素,这就是因为泛型类的类型实参类型被擦除,被 Object 类型代替或上界形参约束类型代替。但是如何去正确检查 value 是否 List 呢?请看以下解决办法:

Java中的解决办法:针对上述的问题,Java 有个很直接解决方式,那就是使用 List 原生类型:

if(value is List){...}

Kotlin中的解决办法:我们都知道 Kotlin 不支持类似 Java 的原生类型,所有的泛型类都需要显示指定类型实参的类型,对于上述问题,Kotlin 中可以借助星投影 List<*> (关于星投影后续会详细讲解)来解决,目前你暂且认为它是拥有未知类型实参的泛型类型,它的作用类似 Java 中的List<?>通配符。

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

特殊情况:我们说 is 检查一般不能检测类型实参,但是有种特殊情况那就是 Kotlin 的编译器智能推导(不得不佩服Kotlin编译器的智能):

fun printNumberList(collection: Collection<String>) {
    if(collection is List<String>){...} //在这里这样写法是合法的。
}

分析:Kotlin 编译器能够根据当前作用域上下文智能推导出类型实参的类型,因为 collection 函数参数的泛型类的类型实参就是 String,所以上述例子的类型实参只能是 String,如果写成其他的类型还会报错呢。

2.2 类型转换问题

在 Kotlin 中我们使用 as 或者 as? 来进行类型转换,注意在使用 as 转换时,仍然可以使用一般的泛型类型。只有该泛型类的基础类型是正确的即使是类型实参错误也能正常编译通过,但是会抛出一个警告。一起来看个例子:

图片描述

package com.mikyou.kotlin.generic


fun main(args: Array<String>) {
    printNumberList(listOf(1, 2, 3, 4, 5))//传入List<Int>类型的数据
}

fun printNumberList(collection: Collection<*>) {
    val numberList = collection as List<Int>//强转成List<Int>
    println(numberList)
}

运行输出:

图片描述

package com.mikyou.kotlin.generic


fun main(args: Array<String>) {
    printNumberList(listOf("a", "b", "c", "d"))//传入List<String>类型的数据
}

fun printNumberList(collection: Collection<*>) {
    val numberList = collection as List<Int>
    //这里强转成List<Int>,并不会报错,输出正常,
    //但是需要注意不能默认把类型实参当做Int来操作,因为擦除无法确定当前类型实参,否则有可能出现运行时异常
    println(numberList)
}

运行输出:

图片描述

如果我们把调用的地方改成 setOf(1,2,3,4,5)

fun main(args: Array<String>) {
    printNumberList(setOf(1, 2, 3, 4, 5))
}

fun printNumberList(collection: Collection<*>) {
    val numberList = collection as List<Int>
    println(numberList)
}

运行输出:

图片描述

分析:仔细想下,得到这样的结果也很正常,我们知道泛型的类型实参虽然在编译期被擦除,泛型类的基础类型不受其影响。虽然不知道 List 集合存储的具体元素类型,但是肯定能知道这是个 List 类型集合不是Set 类型的集合,所以后者肯定会抛异常。

至于前者因为在运行时无法确定类型实参,但是可以确定基础类型。所以只要基础类型匹配,而类型实参无法确定有可能匹配有可能不匹配,Kotlin 编译采用抛出一个警告的处理。

注意:不建议这样的写法是因为容易存在安全隐患,由于编译器只给了个警告,并没有卡死后路。一旦后面默认把它当做强转的类型实参来操作,而调用方传入的是基础类型匹配而类型实参不匹配就会出问题。

package com.mikyou.kotlin.generic


fun main(args: Array<String>) {
    printNumberList(listOf("a", "b", "c", "d"))
}

fun printNumberList(collection: Collection<*>) {
    val numberList = collection as List<Int>
    println(numberList.sum())
}

运行输出:

图片描述

3. 什么是 reified 实化类型参数函数?

通过以上我们知道 Kotlin 和 Java 同样存在泛型类型擦除的问题,但是 Kotlin 作为一门现代编程语言,他知道 Java 擦除所带来的问题,所以开了一扇后门,就是通过 inline 函数保证使得泛型类的类型实参在运行时能够保留,这样的操作 Kotlin 中把它称为实化,对应需要使用 reified 关键字。

3.1 满足实化类型参数函数的必要条件

  • 必须是 inline 内联函数,使用 inline 关键字修饰;
  • 泛型类定义泛型形参时必须使用 reified 关键字修饰;

3.2 带实化类型参数的函数基本定义

inline fun <reified T> isInstanceOf(value: Any): Boolean = value is T 

对于以上例子,我们可以说类型形参 T 是泛型函数 isInstanceOf 的实化类型参数。

3.3 关于inline函数补充一点

我们对 inline 函数应该不陌生,使用它最大一个好处就是函数调用的性能优化和提升,但是需要注意这里使用 inline 函数并不是因为性能的问题,而是另外一个好处它能使泛型函数类型实参进行实化,在运行时能拿到类型实参的信息。至于它是怎么实化的可以接着往下看。

4. 实化类型参数背后原理以及反编译分析

我们知道类型实化参数实际上就是 Kotlin 变得的一个语法魔术,那么现在是时候揭开魔术神秘的面纱了。说实在的这个魔术能实现关键得益于内联函数,没有内联函数那么这个魔术就失效了。

4.1 原理描述

我们都知道内联函数的原理,编译器把实现内联函数的字节码动态插入到每次的调用点。那么实化的原理正是基于这个机制,每次调用带实化类型参数的函数时,编译器都知道此次调用中作为泛型类型实参的具体类型。所以编译器只要在每次调用时生成对应不同类型实参调用的字节码插入到调用点即可。

总之一句话很简单,就是带实化参数的函数每次调用都生成不同类型实参的字节码,动态插入到调用点。由于生成的字节码的类型实参引用了具体的类型,而不是类型参数所以不会存在擦除问题。

4.2 reified 的例子

带实化类型参数的函数被广泛应用于 Kotlin 开发,特别是在一些 Kotlin 的官方库中,下面就用 Anko 库(简化Android的开发kotlin官方库)中一个精简版的 startActivity 函数:

inline fun <reified T: Activity> Context.startActivity(vararg params: Pair<String, Any?>) =
        AnkoInternals.internalStartActivity(this, T::class.java, params)

通过以上例子可看出定义了一个实化类型参数 T,并且它有类型形参上界约束 Activity,它可以直接将实化类型参数T当做普通类型使用。

4.3 代码反编译分析

为了好反编译分析单独把库中的那个函数拷出来取了 startActivityKt 名字便于分析。

class SplashActivity : BizActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.biz_app_activity_welcome)
        startActivityKt<AccountActivity>()//只需这样就直接启动了AccountActivity了,指明了类型形参上界约束Activity
    }
}

inline fun <reified T : Activity> Context.startActivityKt(vararg params: Pair<String, Any?>) =
        AnkoInternals.internalStartActivity(this, T::class.java, params)

编译后关键代码:

//函数定义反编译
 private static final void startActivityKt(@NotNull Context $receiver, Pair... params) {
      Intrinsics.reifiedOperationMarker(4, "T");
      AnkoInternals.internalStartActivity($receiver, Activity.class, params);//注意点一: 由于泛型擦除的影响,编译后原来传入类型实参AccountActivity被它形参上界约束Activity替换了,所以这里证明了我们之前的分析。
   }
//函数调用点反编译
protected void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      this.setContentView(2131361821);
      Pair[] params$iv = new Pair[0];
      AnkoInternals.internalStartActivity(this, AccountActivity.class, params$iv);
      //注意点二: 可以看到这里函数调用并不是简单函数调用,而是根据此次调用明确的类型实参AccountActivity.class替换定义处的Activity.class,然后生成新的字节码插入到调用点。
}

在函数加点输出就会更加清晰:

class SplashActivity : BizActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.biz_app_activity_welcome)
        startActivityKt<AccountActivity>()
    }
}

inline fun <reified T : Activity> Context.startActivityKt(vararg params: Pair<String, Any?>) {
    println("call before")
    AnkoInternals.internalStartActivity(this, T::class.java, params)
    println("call after")
}

反编译后:

private static final void startActivityKt(@NotNull Context $receiver, Pair... params) {
      String var3 = "call before";
      System.out.println(var3);
      Intrinsics.reifiedOperationMarker(4, "T");
      AnkoInternals.internalStartActivity($receiver, Activity.class, params);
      var3 = "call after";
      System.out.println(var3);
   }

   protected void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      this.setContentView(2131361821);
      Pair[] params$iv = new Pair[0];
      String var4 = "call before";
      System.out.println(var4);
      AnkoInternals.internalStartActivity(this, AccountActivity.class, params$iv);//替换成确切的类型实参AccountActivity.class
      var4 = "call after";
      System.out.println(var4);
   }
   

5. 实化类型参数函数的使用限制

5.1 Java 调用 Kotlin 中的实化类型参数函数限制

明确回答 Kotlin 中的实化类型参数函数不能在 Java 中的调用,我们可以简单的分析下,首先 Kotlin 的实化类型参数函数主要得益于 inline 函数的内联功能,但是 Java 可以调用普通的内联函数但是失去了内联功能,失去内联功能也就意味实化操作也就化为泡影。故重申一次 Kotlin 中的实化类型参数函数不能在 Java 中的调用

5.2 Kotlin 实化类型参数函数的使用限制

  • 不能使用非实化类型形参作为类型实参调用带实化类型参数的函数;
  • 不能使用实化类型参数创建该类型参数的实例对象;
  • 不能调用实化类型参数的伴生对象方法;
  • reified关键字只能标记实化类型参数的内联函数,不能作用与类和属性。