Kotlin的独门秘籍Reified实化类型参数(下篇)

Kotlin系列文章,欢迎查看:

原创系列:java

翻译系列:编程

实战系列:安全

简述: 今天咱们开始接着原创系列文章,首先说下为何不把这篇做为翻译篇呢?我看了下做者的原文,里面讲到的,这篇博客都会有所涉及。这篇文章将会带你所有弄懂Kotlin泛型中的reified实化类型参数,包括它的基本使用、源码原理、以及使用场景。有了上篇文章的介绍,相信你们对kotlin的reified实化类型参数有了必定认识和了解。那么这篇文章将会更加完整地梳理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类可看出。app

虽然Kotlin没有和Java同样须要兼容旧版本的历史缘由,可是因为Kotlin编译器编译后出来的class也是要运行在和Java相同的JVM上的,JVM的泛型通常都是经过泛型擦除,因此Kotlin始终仍是迈不过泛型擦除的坎。可是Kotlin是一门有追求的语言不想再被C#那样喷Java说什么泛型集合连本身的类型实参都不知道,因此Kotlin借助inline内联函数玩了个小魔法。编程语言

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

泛型擦除会带来什么影响,这里以Kotlin举例,由于Java遇到的问题,Kotlin一样须要面对。来看个例子ide

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 = {"\u0000\u0014\n\u0000\n\u0002\u0010\u0002\n\u0000\n\u0002\u0010\u0011\n\u0002\u0010\u000e\n\u0002\b\u0002\u001a\u0019\u0010\u0000\u001a\u00020\u00012\f\u0010\u0002\u001a\b\u0012\u0004\u0012\u00020\u00040\u0003¢\u0006\u0002\u0010\u0005¨\u0006\u0006"},
   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类型了。 post

一、类型检查问题:

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,若是写成其余的类型还会报错呢。

二、类型转换问题:

在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关键字。

一、知足实化类型参数函数的必要条件

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

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

inline fun <reified T> isInstanceOf(value: Any): Boolean = value is T 
复制代码

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

三、关于inline函数补充一点

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

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

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

一、原理描述

咱们都知道内联函数的原理,编译器把实现内联函数的字节码动态插入到每次的调用点。那么实化的原理正是基于这个机制,每次调用带实化类型参数的函数时,编译器都知道这次调用中做为泛型类型实参的具体类型。因此编译器只要在每次调用时生成对应不一样类型实参调用的字节码插入到调用点便可。 总之一句话很简单,就是带实化参数的函数每次调用都生成不一样类型实参的字节码,动态插入到调用点。因为生成的字节码的类型实参引用了具体的类型,而不是类型参数因此不会存在擦除问题。

二、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当作普通类型使用

三、代码反编译分析

为了好反编译分析单独把库中的那个函数拷出来取了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、实化类型参数函数的使用限制

这里说的使用限制主要有两点:

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

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

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

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

欢迎关注Kotlin开发者联盟,这里有最新Kotlin技术文章,每周会不按期翻译一篇Kotlin国外技术文章。若是你也喜欢Kotlin,欢迎加入咱们~~~

相关文章
相关标签/搜索