本文灵感来源:群友提出泛型相关的问题,感觉很多人对泛型并不是很了解~
Kotlin中的泛型和Java中的泛型其实大同小异,只是语法稍微有些不同。
大部分内容摘取自: 《Kotlin实用指南》 ,感兴趣的可以订阅一波 ~

0x1、要点提炼

  • 什么是泛型 →「 将确定不变的类型参数化,保证集合中存储元素都是同一种
  • 泛型的好处 →「 编译期类型安全检查 」和「 消除强类型转换,提高可读性
  • Java中泛型的使用 泛型类 泛型接口 泛型方法
  • Java假泛型实现原理 →「 类型擦除(Type Erasure) 」只存在与编译期,进JVM前会擦除类型
  • Java有界泛型类型 限制允许实例化的泛型类型 ,「 extends 父类型
  • Java通配符 <T>与<?> 的区别( 定义-形参 实例化-实参 )
  • 上边界通配符 <? extends 父类型> 父类型未知 ,只能读不能写, 协变
  • 下边界通配符 <? super 子类型> 子类型未知 ,只能写不能读, 逆变
  • 无限定通配符 <?> ,等价于 <? extends Object> ,类型完全未知,只能读不能写, 不变
  • Kotlin中的型变 →「 声明处型变 out(协变)与in(逆变) 类型映射 <*>
  • Kotlin获取泛型类型 匿名内部类 反射 实例化类型参数代替类引用 内联函数
  • 0x2、什么是泛型

    0x3、Java中泛型的使用

    泛型可用于类、接口和方法的创建,对应: 泛型类 泛型接口 泛型方法 ,代码示例如下:

    注意事项

  • 1、泛型的参数只能是「 类类型 」,不能是简单数据类型(int, float等);
  • 2、泛型可以有多个泛型参数
  • Tips 泛型类型的命名不是必须为T ,也可以使用其他「 单个大写字母 」,没有强制的命名规范,但为了便于阅读,有一些约定成俗的命名规范:

  • 通用泛型类型:T,S,U,V
  • 集合元素泛型类型:E
  • 映射键-值泛型类型:K,V
  • 数值泛型类型:N
  • 0x4、Java假泛型实现原理

    和C#中的泛型不同,Java和Kotlin中的泛型都是假泛型,实现原理就是「 类型擦除 (Type Erasure)」。
    Java编译器在生成 Java字节码中是不包含泛型中的类型信息的 ,只存在于 代码编译阶段 ,进JVM前会被擦除。
    不信?写个简单的代码体验下:

    运行结果如下

    从输出结果可以看到获得的类型确实被擦除了,此时的「 类类型 」皆为 ArrayList

    问 → 那我们定义的 泛型类型(Integer, String) 到底去哪了?
    答 → 被替换成了「 原始类型 」(字节码中的真正类型)
    问 → 那原始类型具体是什么类型?
    答 → 「 限定类型 」,无限定的话都用 Object 替换。
    问 → ???
    答 → 且听我娓娓道来~

    还是上面的代码,进Java字节码看看( View -> Show Bytecode )

    的确,Integer和String都被替换成了Object,那这个「 限定类型 」呢?写个例子试试:

    看下 字节码

    行吧,此时的「 原始类型 」为「 限定类型 」即 Animal 类。

    没限定类型的,都替换成Object类,也使得我们可以通过一些操作, 绕过泛型
    比如,我们利用「 反射 」往Integer类型的List插入一个String值是不会报错的:

    运行结果如下

    0x5、Java有界泛型类型

    如果不对泛型类型做限制, 泛型类型可实例化为任意类型 的话,可能会产生某些安全隐患。为了限制允许实例化的泛型类型,可在泛型类型后追加 extends 父类型 ,代码示例如下:

    有界泛型类型在一定程度上限制了泛型类型,提高了程序的安全性;因为定义了边界,所以 可以调用父类或父接口的方法

    0x6、Java通配符

    Java泛型本身「 不变 」的,不支持「 协变 」和「 逆变 」,是通过通配符(?)来实现的

    ① <T>与<?>的区别

    <T> 泛型标识符 」用于泛型「 定义时 」可理解为「 形参 」;
    <?> 通配符 」用于泛型「 实例化时 」可理解为「 实参 」。

    代码示例如下

    ② 各种通配符

  • 上边界通配符<? extends 父类型>
    实例化时可确定为「 父类型的未知类型 」故「 只能读不能写 」,从而使得类型是「 协变的 」。
  • 代码示例如下

  • 下边界通配符:<? super 子类型>
    实例化时可确定为「 子类型的未知类型 」故「 只能写不能读 」从而使得类型是「 逆变的 」。
    (不能读指的是 不能按照泛型类型读取 )< 代码示例如下:
  • 上边界通配符只能读不能写,改为下边界通配符

  • 无限定通配符:<?>
    等同于上边界通配符 <? extends Object> ,「 类型完全未知 」故「 只能读不能写 」从而使得类型是「 不变的 」。
  • Tips :看完这里,估计有部分读者还是有点懵逼,这里总结下:

  • 不变 → 子类和父类没关系,正常存取,不用通配符就好,Java集合本身就是不变的;
  • 协变 → A是B的子类型,泛型<A>也是泛型<B>的子类型,只想取,用extends。
  • 逆变 → A是B的子类型,泛型<B>也是泛型<A>的子类型,只想存,用super。
  • PECS法则 (Producer Extends,Consumer Super) 参数化类型 是一个生产者,则使用:<? extends T> 如果它是一个消费者,则使用<? super T>
  • 0x7、Kotlin中的型变

    和Java泛型一样,Kotlin中的泛型也是「 不变的 」,没有「 通配符类型 」,但有两个其他的东西:「 声明处型变 」(declaration-site variance)  与 「 类型投影 」(type projections)

    ① 声明处型变

    其实就是用「 out 」和「 in 」关键字来替换

  • out 协变 ,等同于Java中的<? extends>, 能读不能写 ,代码示例如下:
  • in 逆变 ,等同于Java中的<? super>, 能写不能读 ,代码示例如下:
  • ② 类型投影

    其实就是对应Java中的*通配符:

  • Java中<?>等同于<* extends Object>
  • Kotlin中 <*> 等同于 out Any
  • 0x7、Kotlin获取泛型类型

    在Kotlin中可以通过下述四种方法获取泛型的类型(前两种Java也适用):

    ① 匿名内部类

    原理: 匿名内部类的声明在编译时进行,实例化在运行时进行 代码示例如下

    获取运行时泛型参数类型 ,子类可获得父类泛型的具体类型。 代码示例如下

    ③ 实例化类型参数代替类引用

    定义一个扩展函数用于启动Activity,代码示例如下:

    fun <T: Activity> FragmentActivity.startActivity(context: Context, clazz: Class<T>) {
        startActivity(Intent(context, clazz))
    // 调用
    startActivity(context, MainActivity::class.java)
    

    ④ 内联函数

    Kotlin中使用「inline」关键字定义一个内联函数,配合「reified」具体化(类型不擦除),得到使用泛型类型的Class。修改后的代码示例如下:

    inline fun <reified T : Activity> Activity.startActivity(context: Context) {
        startActivity(Intent(context, T::class.java))
    // 调用
    startActivity<MainActivity>(context)
    

    参考文献

  • 《Java泛型学习系列-绪论》
  • 《Android 进阶:基于 Kotlin 的 Android App 开发实践》

  • 分类:
    Android
    标签: