服务端相关 / Kotlin 初识泛型型变

Kotlin 初识泛型型变

相信有很多初学者对 Kotlin 中的泛型型变都是一知半解,泛型型变概念太多了,而且每个概念和后面都是相关的,只要前面有一个地方未理解后面的难点更是越来越看不懂。Kotlin 的泛型比 Java 中的泛型多了一些新的概念,比如子类型化关系、逆变、协变、星投影的。个人认为学好 Kotlin 的泛型主要有这么几个步骤:

  • 深入理解泛型中每个小概念和结论,最好能用自己的话表述出来;
  • 通过分析 Kotlin 中的相关源码验证你的理解和结论;
  • 通过实际的例子巩固你的理解。

1. 泛型为什么会存在型变?

首先,我们需要明确两个名词概念: 基础类型和实参类型。例如对于 List<String>, List 就是基础类型而这里的 String 就是实参类型

然后,我们需要明确一下,这里的型变到底指的是什么?可以先大概描述一下,它反映的是一种特殊类型的对应关系规则。是不是很抽象?

那就先来看个例子,例如 List<String>和List<Any> 他们拥有相同的基础类型,实参类型 StringAny 存在父子关系,那么是不是 List<String>List<Any> 是否存在某种对应关系呢?实际上,我们讨论的型变也就是围绕着这种场景展开的。有了上面的认识,进入正题为什么需要这种型变关系呢?来看对比的例子,我们需要向一个函数中传递参数。

fun main(args: Array<String>) {
    val stringList: List<String> = listOf("a", "b", "c", "d")
    val intList: List<Int> = listOf(1, 2, 3, 4)
    printList(stringList)//向函数传递一个List<String>函数实参,也就是这里List<String>是可以替换List<Any>
    printList(intList)//向函数传递一个List<Int>函数实参,也就是这里List<Int>是可以替换List<Any>
}

fun printList(list: List<Any>) {
//注意:这里函数形参类型是List<Any>,函数内部是不知道外部传入是List<Int>还是List<String>,全部当做List<Any>处理
    list.forEach {
        println(it)
    }
}

上述操作是合法的,运行结果如下:
图片描述
如果我们上述的函数形参 List<Any> 换成 MutableList<Any> 会变成什么样呢?

fun main(args: Array<String>) {
    val stringList: MutableList<String> = mutableListOf("a", "b", "c", "d")
    val intList: MutableList<Int> = mutableListOf(1, 2, 3, 4)
    printList(stringList)//这里实际上是编译不通过的
    printList(intList)//这里实际上是编译不通过的
}

fun printList(list: MutableList<Any>) {
    list.add(3.0f)//开始引入危险操作dangerous! dangerous! dangerous!
    list.forEach {
        println(it)
    }
}

图片描述

我们来试想下,利用反证法验证下,假如上述代码编译通过了,会发生什么,就会发生下面的可能出现类似的危险操作。就会出现一个 Int 或者 String 的集合中引入其他的非法数据类型,所以肯定是有问题的,故编译不通过。

因为我们说过在函数的形参类型 MutableList<Any> 在函数内部它只知道是该类型也不知道外部给它传了个啥,所以它只能在内部按照这个类型规则来,所以在函数内部 list.add(3.0f) 这行代码时编译通过的,向一个 MutableList<Any> 集合加入一个 Float 类型明显说得过去的。

通过对比上面两个例子,大家有没有思考一个问题就是为什么 List<String>、List<Int>替换List<Any> 可以,而 MutableList<String>、MutableList<Int>替换MutableList<Any> 不可以呢?

实际上问题所说的类型替换其实就是型变 , 那大家到这就明白了为什么会存在型变了,型变更为了泛型接口更加安全,假如没有型变,就会出现上述危险问题。

那另一问题来了为什么有的型变关系可以,有的不可以呢?对于传入集合内部不会存在修改添加其元素的操作 (只读),是可以支持外部传入更加具体类型实参是安全的,而对于集合内部存在修改元素的操作 (写操作) 是不安全的,所以编译器不允许。

以上面例子分析,List<Any> 实际上一个只读集合 (注意:它和 Java 中的 List 完全不是一个东西,注意区分),它内部不存在 add,remove 操作方法,不信的可以看下它的源码,所以以它为形参的函数就可以敞开大门大胆接收外部参数,因为不存在修改元素操作所以是安全的,所以第一个例子是编译 OK 的;

而对于 MutableList<Any> 在 Kotlin 中它是一个可读可写的集合,相当于 Java 中的 List, 所以它的内部存在着修改、删除、添加元素的危险操作方法,所以对于外部传入的函数形参它需要做严格检查必须是 MutableList<Any> 类型。

为了帮助理解和记忆,自己绘制了一张独具风趣的漫画图帮助理解,这张图很重要以致于后面的协变、逆变、不变都可以从它获得理解。后面也会不断把它拿出来分析
图片描述

最后为了彻底把这个问题分析透彻可以给大家看下 List<E>MutableList<E> 的部分源码

图片描述

public interface List<out E> : Collection<E> {
    // Query Operations
    override val size: Int

    override fun isEmpty(): Boolean
    override fun contains(element: @UnsafeVariance E): Boolean
    override fun iterator(): Iterator<E>

    // Bulk Operations
    override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean
    
    ...
  }

图片描述

public interface MutableList<E> : List<E>, MutableCollection<E> {
    // Modification Operations
    override fun add(element: E): Boolean

    override fun remove(element: E): Boolean

    // Bulk Modification Operations
    override fun addAll(elements: Collection<E>): Boolean
    ...
 }

仔细对比下 List<out E>MutableList<E> 泛型定义是不一样的,他们分别对应了协变不变 , 至于什么是协变什么是逆变什么不变,我们后面会详细讲。

2. 子类、子类型、超类型概念梳理

我们一般说子类就是派生类,该类一般会继承它的超类。例如: class Student: Person(), 这里的 Student 一般称为 Person 的子类,PersonStudent 的超类。

子类型和超类型定义则完全不一样,我们从上面类和类型区别就知道一个类可以有很多类型,那么子类型不仅仅是想子类那样继承关系那么严格。

子类型定义的规则一般是这样的: 任何时候如果需要的是 A 类型值的任何地方,都可以使用 B 类型的值来替换的,那么就可以说 B 类型是 A 类型的子类型或者称 A 类型是 B 类型的超类型。可以明显看出子类型的规则会比子类规则更为宽松。那么我们可以一起分析下面几个例子:

图片描述

Tips:某个类型也是它自己本身的子类型,很明显 Person 类型的值任意出现地方,Person 肯定都是可以替换的。属于子类关系的一般也是子类型关系。像 String 类型值肯定不能替代 Int 类型值出现的地方,所以它们不存在子类型关系

再来看个例子,所有类的非空类型都是该类对应的可空类型的子类型,但是反过来说就不行,就比如 Person 非空类型是 Person? 可空类型的子类型,很明显嘛,任何 Person? 可空类型出现值的地方,都可以使用 Person 非空类型的值来替换。

其实这些我在开发过程中是可以体会得到的,比如细心的同学就会发现,我们在 Kotlin 开发过程,** 如果一个函数接收的是一个可空类型的参数,调用的地方传入一个非空类型的实参进去是合法的。** 但是如果一个函数接收的是非空类型参数,传入一个可空类型的实参编译器就会提示你,可能存在空指针问题,需要做非空判断。因为我们知道非空类型比可空类型更安全。来幅图理解下:

图片描述

3. 什么是子类型化关系

相信到了这,大家应该自己都能猜出什么是子类型化关系吧?它是实际上就是我们上面所讲的那些。

3.1 子类型化关系

大致概括一下: 如果 A 类型的值在任何时候任何地方出现都能被 B 类型的值替换,B 类型就是 A 类型的子类型,那么 B 类型到 A 类型之间这种映射替换关系就是子类型化关系

3.2 回答最开始的问题

现在我们也能用 Kotlin 中较为专业的术语子类型化关系来解释最开始那个问题为什么以 List<String>,List<Int> 类型的函数实参可以传递给 List<Any> 类型的函数形参,而 MutableList<String>,MutableList<Int> 类型的函数实参不可以传递给 MutableList<Any> 类型的函数形参?

因为 List<String>,List<Int> 类型是 List<Any> 类型的子类型,所以 List<Any> 类型值出现的地方都可以使用 List<String>,List<Int> 类型的值来替换。而 MutableList<String>,MutableList<Int> 类型不是 MutableList<Any> 的子类型也不是它的超类型,所以当然就不能替换了。

3.2 由上面回答引出一个细节点

仔细分析观察下上面所说的,List<String>,List<Int> 类型是 List<Any> 类型的子类型,然后再细看针对都具有相同的 List 这个基础类型的泛型参数类型对应关系,这里的 String,Int 类型是 Any 类型的子类型 (注意:我们在泛型中都应该站在类型和子类型的角度来看问题,不要在局限于类和子类继承层面啊,这点很重要,因为 List<String> 还是 List<String?> 子类型呢,所以和继承层面子类没有关系),然后 List<String>,List<Int> 类型也是 List<Any> 类型的子类型,这种关系叫做保留子类型化关系,也就是所谓的协变。具体下篇文章着重分析。

4. 总结

本篇文章可以说是下篇文章的一个概念理解的基础,下篇很多高级的概念和原理都是在这篇文章延伸的,建议好好消化这些概念,这里最后再着重强调一下:一定需要好好理解什么是子类型,它和子类有什么区别。实际上 Kotlin 中的泛型型变的基础就是子类型化关系,一般在这我们都是站在类型和子类型角度分析关系,而不是简单的类和子类继承层面。还有就是有没有思考过为什么要弄这么一套复杂的型变关系,其实仔细想想就为了泛型类操作和使用更加安全,避免引入一些存在危险隐患。下篇文章就是泛型中的高级概念,其中包括泛型协变、逆变等,只要把这篇文章概念理解清楚了后面会很简单的。