九、kotlin的泛型

537 阅读14分钟
theme: Chinese-red

kotlin的泛型基础和 java 很像, 所以我建议学习 kotlin 的泛型前, 先去学习下 java 的泛型, 至少搞懂通配符, <? extends X><? super X> 是怎么回事, 怎么写 泛型函数, 泛型类, 知道泛型的本质是什么?

泛型

泛型: 将类型当作参数传递

参数泛型
fun funName(参数)class ClassName<类型>
传递给函数传递给对象

我们需要将类型当作参数传递给对象, 传递的类型可能会被用于定义属性或者用于函数的泛型参数

需要注意:

参数有可变参数 vararg 泛型也是, 可以传递泛型的子类类型

简单示例: 函数, 参数, 属性和类的泛型

 fun <T> print(t: T) {
    println(t)
 }
 ​
 class GenericsDemo01<T>(val f: T) {
    fun print(t: T) {
       println(t)
    }
 }

泛型约束(T : Integer)

主要内容

  1. 缩小类型的范围
  2. 一个约束 T : Integer ==> T extends Integer
  3. 多个约束 T where T: XXX, T: YYY ==> T extends CharSequence & Appendable

很多时候我们需要将泛型的类型约束在某个界限, 比如: sum函数的泛型

 fun <T> sum(a: T, b: T): T {
     return a + b // error
 }

参数 a 和 参数 b 并不是什么类型都支持 + 这项操作, 所以我们需要对传入的类型参数(泛型)做限制, 像下面这样

 fun <T : Integer> sum(a: T, b: T): T {
     return a + b
 }

这样操作类似于 java 的 <T extends Integer> , 限定 T 必须继承 Integer(或者说T必须是Integer的子类).

java 的<T extends Integer> 用于集合的泛型, 而泛型约束通常用于非集合的泛型, 因为集合泛型已经有 协变和逆变 的约束了, 不需要这一章的泛型约束

对的, 这样做就不会出现传入俩 Any 类型ab 做加法运算符这样尴尬的事情

泛型约束不会像集合泛型约束那样严格控制 T 必须是同一个, 你可以这样使用:

 private fun <T : Number> printT(a: T, b: T) {
    // a = 9999, b = 100.5
    println("a = $a, b = $b")
    // aClass = class java.lang.Integer, bClass = class java.lang.Double
    println("aClass = ${a.javaClass}, bClass = ${b.javaClass}")
 }
 ​
 fun main() {
    printT(9999, 100.5)
 }

a: T, b: T 中的 T 是两个不一样的类型, 一个是 Integer, 另一个是 Double

上面那段代码类似于 java 的这段代码

 static <T extends Number> void printT(T a, T b) {
    System.out.println("a = " + a);
    System.out.println("b = " + b);
    System.out.println(a.getClass());
    System.out.println(b.getClass());
 }
 ​
 public static void main(String[] args) throws Exception {
    printT(10, 20.1);
 }

课外: 突然发现 不知道 泛型如何 写 sum 了, 所以想了下, 好像只能使用反射来实现

 fun <T> sum(a: T, b: T): T {
    val clazz = a.javaClass
    val sum = clazz.declaredMethods.firstOrNull { it.name == "sum" } ?: throw Exception("can't find function. T not is a subclass of Number")
    return sum.invoke(a, a, b) as T
 }

小笔记: 在反射获取 sum 函数时, 发现它的函数签名是: int sum(int, int) 但是我们a: T 类型的 T 会被认为是 Integer(泛型只能是Integer), 而获取 sum 却需要 int 比较麻烦, 最后发现 Integer.TYPE 是拆包类型int 可以考虑从这里下手

为一个泛型添加多个约束

where 类似于 sql 语句的 where 一样

 fun <T> ensureTrailingPeriod(seq: T): T where T : CharSequence, T : Appendable {
    if (!seq.endsWith(".")) {
       seq.append(".")
    }
    return seq
 }

约束的好处不仅仅是让我们知道我们需要的类必须是约束和约束的子类. 同时还会让我们的 T 多出很多约束类的函数(包括扩展函数等)

image.png

endsWith 函数是 CharSequence 的扩展函数, appendAppendable 接口的函数

前面的 sum 函数也是

image.png

java中也允许多约束泛型

 static <T extends CharSequence & Appendable> T ensureTrailingPeriod(T seq)

泛型类型可以为 null 也可以为 non-null

泛型的类型T类似于平台类型, 是否为空由程序员决定, kotlin 不再进行可空管理, 程序员认为他是 可空类型 它就是可空类型, 程序员认为它非空类型, 它就是非空类型

 fun <T> print(t: T) {
    t?.let { println(it) }
 }

泛型运行时的类型擦除实化类型

主要内容:

  1. 类型擦除 和 java 一样(妥妥的糟粕, 给整过来了)
  2. 实化类型: 用于 is TT::class.java
 fun <T> isIntList(list: List<T>) {
    if (list is List<Int>) { // 这里报错
       println("这是错误的")
    }
 }
 ​
 fun main() {
    val list = listOf(1, 2, 3)
    isIntList(list)
 }

image.png

但是可以这样:

 if (list is List<*>)

可以看出, 泛型被类型擦除为 List<Any?>

 fun <T> isIntList(list: List<T>) {
    if (list is List<Any?>) { // 这样不会报错
       println("不会报错了")
    }
 }

kotlin 编译器可以判断在同一个作用域内的泛型类型

 val list = listOf(1, 2, 3)
 if (list is List<Int>) {
     println("这样是可以的")
 }

下面这种情况也会出现问题

image.png

实化类型参数 reified T

在运行期间, 类型被当作 Any? 类型, 但它想被强制转换成 T 类型明显是不行的, 这种情况下, 可以考虑使用 inlinereified 配合实现

 inline fun <reified T> isIntList(list: List<T>) {
    if (list is List<T>) { // 这里不会报错
       println("不会报错")
    }
 }

同样的我们调用 is T 也不会报错了

 inline fun <reified T> isA(value: Any) = value is T
 ​
 fun main() {
    val a: Int = 10
    println(isA<Int>(a))
 }

前面我们学过, inline 内联的话, 会把代码拷贝到所有调用的地方, 使用上面这种方式kotlin编译器在运行期间可以识别到泛型的类型

inline 在之前的章节中是为了提高性能, 消除lambda参数带来的副作用对象而使用的, 在本章节是为了实化类型, 这是第二个inline 的使用场景

实例化参数的另一种使用场景是, 将 类型做参数传递后, 借助该类型获取 Class 类对象

 inline fun <reified T> loadService(): ServiceLoader<T>? {
    return ServiceLoader.load(T::class.java)
 }
 ​
 fun main() {
    // 以前需要在参数上多一个传递 Class 的参数, 现在不需要了
    // val loadService = loadService(Int::class)
    val loadService = loadService<Int>()
 }

以前需要在参数上多一个传递 Class 的参数, 现在不需要了

变型: 泛型和子类型关系

类、类型和子类型

类和类型的区别

在很多情况下, 类都可以大体上当作类型, 但实际上, 类和类型不是一个东西就比如: 空类型和非空类型, IntInt? , 请确认下 Int? 是类么?? 不是 那Int? 是类型么? 明显,是类型

又或者: List是个类而 List<T> 它又是个类型, 且他的类型有很多, 比如: List<Int> List<Double> List<Long> 等, 这些都是类型, 而List

子类型关系

子类型说的是一种 父子关系 , 这种关系在 java 的类, java 的数组里存在, 而在 java 的泛型里却不见了

(书本上的内容, 看不懂看下面)任何时候如果需要的是类型 A 的值,你都能够使用类型 B 的值当作 A 使用 , 类型 B 就称为类型 A 的子类型。

说简单点, A类指针(java叫引用)指向B类对象, 那么就可以说 A 的子类型是 B, 就这么简单(val a: A = B())

比如: 现在有个引用 val a: Number和一个Int类型的对象10, 如果引用能够直接指向对象val a: Number = 10 则可以说 IntNumber子类型, 而同时我们可以说 NumberInt超类型

简单点: 子类的超类型是父类, 父类的子类型是子类, 只要记住这种关系就好

协变和逆变

高端的概念总会有落地的实现, 我们学习要达到的程度是用最简单的一句话描述这些概念

在生活中, 越宽的桶能够盛放越多的水, 越小的桶能够盛放越少的水, 而我们的类型也是, Any? 是 kotlin 中最宽泛的水桶, 它既能够存放非空的所有对象, 也能够存放可空的所有kotlin对象, 这就是多态的根本, 也是协变和逆变的根本

协变(covariant)

1. 是什么?

协变: 是一种关系, 一种父类引用 指向 子类对象 的关系

  • Number 引用总能够指向 Int 对象, 那么 Number 的子类型是 Int , 则 NumberInt 是协变的
  • 那么同样的 Number[] 引用 总能够 指向 Int[] , 则 Number[]Int[] 是协变的
  • 同样的, List<Number> 的引用总能够 指向 List<Int> 那么我们也能够说: List<Number>List<Int> 有协变关系(但在java中失效了)

但, java 因为历史关系, 使用了类型擦除技术, 所以 任何类型变到泛型的话, 就不会有所谓的协变(逆变)关系, 因为到了运行时期 java 总把类型变成 List<Object> 或者 直接是 List 类型, 如果强制开出协变关系, 则会出现一些安全问题

java泛型类型擦除带来的问题

会出现 哥哥 泛型的水桶, 被jvm拿走忘了只能装哥哥了, 装了个 弟弟 泛型类型的对象, 这明显不对, 我的水桶要的只能是 哥哥 或者 哥哥的子类, 最最重要的是 jvm 记性还不好(类型擦除), 会把所有装XXX的水桶, 记成水桶里什么东西都能装

泛型在存在协变关系的数组中, 可以正确的判断出错误:

 Integer[] a = new Integer[2];
 a[0] = 1000;
 Object[] o = a;
 o[1] = 'a'; // 这里会报错 java.lang.ArrayStoreException: java.lang.Character   

Integer 引用想指向没有子类型关系的 Character对象, 直接报错

如果把上面的数组完全换成集合就会变成如下代码:

 List<Integer> list = new ArrayList();
 list.add(1000);
 List<Object> objList = list; // 父类引用指向子类对象, 按理来说 没错 object --> Integer(但实际上这里不会编译通过的)
 objList.add(10.9); // 这里在运行期间将会编译通过, 运行通过, 因为还是 父类引用指向子类的对象, object --> double

image.png

对比下有协变的数组:

image.png

这种泛型和数组的不一致就表示泛型不存在协变关系

为了解决上面的问题, java 引入了属于 java 的泛型的协变

java泛型对于"消失的协变关系"的解决方案

协变关系, 又有人叫 子类型关系

java 引入了 通配符?, 然后用 List<? extends Number> 表示协变, 相当于没有类型擦除List<Number>, 接受NumberNumber的子类存入List<Number> 集合中

所以 List<? extends Number> 集合可以存入

image.png

上面这些类的对象

那么他是如何解决的上面那个问题的呢?

答: java 的解决方法很简单, 一刀切, 如果类型是 <? extends Number> 协变的, 那么他就不允许写入, 修改等操作. 只允许读取

image.png

我特么, 解决不了问题, 就解决提出问题的人是吧???

小总结: List<? extends Number> 不好记里面可以存放什么类, 可以直接认为是 支持协变的 List<Number> 理解就好了, 支持协变的话, Number 集合可以存入它和它的子类

当然我们也可以认为?就是我们写的类, class ? extends Number {} 表示写了个Number的子类, 意味着?是子类, 所以?表示所有的子类

对应于 kotlin 的协变关系

kotlin 中, 协变将会是: 1. 在类处类型参数协变 2. 在函数处集合泛型的协变

类处类型参数的协变
 interface Producer<out T> {
     fun produce() : T
 }

out 放在那里的位置, 主要有两个功能:

  • 子类型将会被保留(Producer<Cat>Producer<Animal>的子类)
  • T 只能用在 out 位置

image.png

in 的位置在函数参数, out 位置在函数返回值, 既是in又是out则不需要标记, 同样的 out 标记的泛型只能读取, 不能写入, in标记的泛型只能写入不能读取(和java优点不太一样???)

上面的transform函数, 参数明显是范围越大越好, 所以使用 ? super Number也就是kotlin中的in T, 而通过函数transform函数处理之后返回的范围应该越小越好, 所以使用? extends Number 也就是out T

  1. MutableList不能使用out, 因为out只能往外输出(读取)对象而不能往里写入对象, 但MutableList可以写入可以读取, 明显矛盾
  2. 协变后的集合不允许写入, 只允许读取
  3. 协变的out B表示只能填入B或者B的子类
函数处集合泛型的协变

和 java 类似的用法

out T 对应了 java 的 ? extends T

in T 对应了 java 的 ? super T

我们使用下面的代码来了解协变的一些特性

 open class A
 open class B : A()
 open class C : B()
 open class D : C()
 class E
  1. 首先协变out B可以看作是? extends B也就是所谓的上界, 说白了只能接受B以及B的子类

     val l0: ArrayList<out B> = arrayListOf(B(), C(), D())
     // 但如果我们加添 B 的父类 A 对象试试
     val l1: ArrayList<out B> = arrayListOf(A(), B(), C(), D()) // error
     // 这里就会报错, 无法添加高于B的对象
    

    虽然可以这么写, 但最好别这么用, 协变在调用函数传参的时候才能得到充分的体现

  2. 协变无法添加元素

     val l0: ArrayList<out B> = arrayListOf(B())
     l0.add(C()) // error, 无法再次添加对象
    

    再次声明: val l0: ArrayList<out B> = arrayListOf(B()) 虽然运行这么写, 但最好不要这么用

  3. 看下面的f1f2函数

image.png

但是可以这么传递:

image.png

逆变(contravariant): 相反的子类关系

正常情况下, AnimalCat 的父类, Animal 的子类型是 Cat , List<Animal>也是List<Cat>的子类型, 这是协变, 但如果 List<Cat>List<Animal>的子类型的话, 这种子类型关系逆反了, 这就是逆变

研究逆变需要了解两个步骤

  • 初始化阶段

    初始化阶段List<in Cat> 可以看作是Any类型

  • 使用阶段

    在使用的时候, in Cat变成了List<Cat>

image.png

image.png

kotlin 中的逆变

同样的 kotlin 支持: 1. 类的泛型参数逆变 2. 函数集合参数泛型逆变

类的泛型参数逆变
 class A<in T> {
    fun write(t: T) {
    }
 }
函数集合参数泛型逆变

image.png

逆变在调用函数并传参的时候得到体现, 而在使用逆变后的对象添加参数时, 又恢复了 父类指针指向子类对象的赋值兼容性原则

协变逆变的总结

父类 ==> 当前类 ==> 当前类子类

|--> 👆 -->

👆 这里就是上界的边界

协变: 规定了泛型(类型)上界(上边界), 该上限限定了只能传递某个类型及该类型的子类()

父类 ==> 当前类 ==> 当前类子类

Any --> 👆 <--|

👆 这里就是下界的边界

逆变: 规定了下界(下边界), 规定了只能传递某个类型及该类型的父类

在 kotlin 中, 如果泛型被标记为 out, 则该泛型只能调用符合泛型 out 位置的函数, 比如fun get() : T, 如果泛型被标记为 in, 那么只能调用该类的复合 in 位置的函数比如: fun add(t: T): void

out 协变, 只读, in 逆变, 能读写

使用协变和逆变写个 copyData 函数

  1. 普通方式实现该函数
 fun <T> copyData01(source: MutableList<T>, destination: MutableList<T>) {
    for (item in source) {
       destination.add(item)
    }
 }
  1. 使用约束的方式实现该函数
 /**
  * T 是 R 的子类或者 T 就是 R,  记作: T <= R
  * 所以 source: MutableList<T> 是子类集
  * destination: MutableList<R> 是父类集
  * 把子类集source的 item 依次给 父类集的 destination
  */
 fun <T : R, R> copyData02(source: MutableList<T>, destination: MutableList<R>) {
    for (item in source) {
       // T 是子类(source)
       // R 是父类(destination)
       // R ==> T   父类 指向 子类
       // destination ==> source  父类 指向 子类
       destination.add(item)
    }
 }

这种方式不好左区分, 到底哪个是父类, 哪个是子类, 哪个是输出, 哪个是输入

  1. 使用协变的方式实现函数
 /**
  * 对读取函数使用 out 泛型修饰符
  * out T 表示 T 或者 T 的子类
  */
 fun <T> copyData03(source: MutableList<out T>, destination: MutableList<T>) {
    for (item in source) {
       destination.add(item)
    }
 }
 ​
 /**
  * in T: T 的父类
  */
 fun <T> copyData04(source: MutableList<T>, destination: MutableList<in T>) {
    for (item in source) {
       destination.add(item)
    }
 }
 ​
 /**
  * 下面这就是声明处变型
  */
 fun <T> copyData05(source: MutableList<out T>, destination: MutableList<in T>) {
    for (item in source) {
       destination.add(item)
    }
 }
 ​
 /**
  * List 本身就是只读的, 所以看 List 源码的话会看到 public interface List<out E> 这段代码
  * 看到 out E 了么?
  */
 fun <T> copyData06(source: List<T>, destination: MutableList<in T>) {
    for (item in source) {
       destination.add(item)
    }
 }

source: MutableList<out T>, destination: MutableList<in T> 这种方式能够很明显的发现哪个是输出, 哪个是输入

泛型类中 outin 的位置

image.png

kotlin 支持在 类声明 处定义泛型的 变型 , 也支持像 java 一样在 函数位置写上 变型

星号投影: 使用 * 代替类型参数

  1. 星号投影不清楚存入的类型到底是哪个, 所以一般不做写入, 仅作读取

所以功能上类似于 List<out Any?>, 在没有任何类型信息的情况下, Any 是最好的选择

  1. 使用星号投影的, 说明开发者并不需要知道读取出来的泛型具体是什么类型

说白一点, 星号投影把它当作 out Any? 吧, 读取出来的对象当作 Any? 对象就行, 不能写入