简述: Kotlin中泛型相关的文章也几乎接近尾声,但到后面也是泛型的难点和重点。相信有不少初学者对Kotlin中的泛型型变都是只知其一;不知其二,好比我刚开始接触就是一脸懵逼,概念太多了,并且每一个概念和后面都是相关的,只要前面有一个地方未理解后面的难点更是愈来愈看不懂。Kotlin的泛型比Java中的泛型多了一些新的概念,好比子类型化关系、逆变、协变、星投影的。我的认为学好Kotlin的泛型主要有这么几个步骤:安全
因为泛型型变涉及的内容比较多,因此将它分为上下两篇,废话很少说请看如下导图:app
首先,咱们须要明确两个名词概念: 基础类型和实参类型。例如对于List<String>
, List
就是基础类型而这里的String
就是实参类型ide
而后,咱们须要明确一下,这里的型变到底指的是什么?函数
能够先大概描述一下,它反映的是一种特殊类型的对应关系规则。是否是很抽象?那就先来看个例子,例如List<String>和List<Any>
他们拥有相同的基础类型,实参类型String
和Any
存在父子关系,那么是否是List<String>
和List<Any>
是否存在某种对应关系呢? 实际上,咱们讨论的型变也就是围绕着这种场景展开的。post
有了上面的认识,进入正题为何须要这种型变关系呢?来看对比的例子,咱们须要向一个函数中传递参数。ui
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)
}
}
复制代码
上述操做是合法的,运行结果以下spa
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>
泛型定义是不同的,他们分别对应了协变和不变,至于什么是协变什么是逆变什么不变,咱们后面会详细讲。
看到标题可能你们会有点纳闷, 类和类型不是一个东西吗?我平时都是把它们当作一个东西来用的啊。其实是不同的,在这里咱们须要去一一扣概念去理解,以便后面更好理解型变关系。那么咱们一块儿看下它们到底有哪些不同的?
咱们能够把Kotlin中的类可分为两大类: 泛型类和非泛型类
先说非泛型类也就是开发中接触最多的通常类,通常的类去定义一个变量的时候,它的类实际就是这个变量的类型。例如: var msg: String
这里咱们能够说String
类和msg
变量的类型是一致的。可是在Kotlin中还有一种特殊的类型那就是可空类型,能够定义为var msg: String?
,这里的String
类和msg
变量的String?
类型就不同了。因此在Kotlin中一个类通常至少对应两种类型. 因此类和类型不是一个东西。
泛型类比非泛型类要更加复杂,实际上一个泛型类能够对应无限种类型。为何这么说,其实很容易理解。咱们从前面文章知道,在定义泛型类的时候会定义泛型形参,要想拿到一个合法的泛型类型就须要在外部使用地方传入具体的类型实参替换定义中的类型形参。咱们知道在Kotlin中List
是一个类,它不是一个类型。由它能够衍生成无限种泛型类型例如List<String>、List<Int>、List<List<String>>、List<Map<String,Int>>
咱们通常说子类就是派生类,该类通常会继承它的父类(也叫基类)。例如: class Student: Person()
,这里的Student通常称为Person的子类。
而子类型则不同,咱们从上面类和类型区别就知道一个类能够有不少类型,那么子类型不只仅是想子类那样继承关系那么严格。 子类型定义的规则通常是这样的: 任什么时候候若是须要的是A类型值的任何地方,均可以使用B类型的值来替换的,那么就能够说B类型是A类型的子类型或者称A类型是B类型的超类型。能够明显看出子类型的规则会比子类规则更为宽松。那么咱们能够一块儿分析下面几个例子:
注意: 某个类型也是它本身自己的子类型,很明显String类型的值任意出现地方,String确定都是能够替换的。属于子类关系的通常也是子类型关系。像String类型值确定不能替代Int类型值出现的地方,因此它们不存在子类型关系
再来看个例子,全部类的非空类型都是该类对应的可空类型的子类型,可是反过来讲就不行,就好比String
非空类型是String?
可空类型的子类型,很明显嘛,任何String?
可空类型出现值的地方,均可以使用String
非空类型的值来替换。其实这些我在开发过程当中是能够体会获得的,好比细心的同窗就会发现,咱们在Kotlin开发过程,若是一个函数接收的是一个可空类型的参数,调用的地方传入一个非空类型的实参进去是合法的。可是若是一个函数接收的是非空类型参数,传入一个可空类型的实参编译器就会提示你,可能存在空指针问题,须要作非空判断。 由于咱们知道非空类型比可空类型更安全。来幅图理解下:
我相信到了这,你们应该本身都能猜出什么是子类型化关系吧?它是实际上就是咱们上面所讲的那些。
大体归纳一下: 若是A类型的值在任什么时候候任何地方出现都能被B类型的值替换,B类型就是A类型的子类型,那么B类型到A类型之间这种映射替换关系就是子类型化关系
如今咱们也能用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>
的子类型也不是它的超类型,因此固然就不能替换了。
仔细分析观察下上面所说的,List<String>,List<Int>
类型是List<Any>
类型的子类型,而后再细看针对都具备相同的List
这个基础类型的泛型参数类型对应关系, 这里的String,Int
类型是Any
类型的子类型(注意: 咱们在泛型中都应该站在类型和子类型的角度来看问题,不要在局限于类和子类继承层面啊,这点很重要,由于List<String>
仍是List<String?>
子类型呢,因此和继承层面子类没有关系),而后List<String>,List<Int>
类型也是List<Any>
类型的子类型,这种关系叫作保留子类型化关系,也就是所谓的协变。具体我会下篇着重分析。
本篇文章能够说是下篇文章的一个概念理解的基础,下篇不少高级的概念和原理都是在这篇文章延伸的,建议好好消化这些概念,这里最后再着重强调几点:
一、必定须要好好理解什么是子类型,它和子类有什么区别。实际上Kotlin中的泛型型变的基础就是子类型化关系啊,通常在这咱们都是站在类型和子类型角度分析关系,而不是简单的类和子类继承层面啊。
二、还有就是你们有没有思考过为何要弄这么一套型变关系啊,其实仔细想一想就为了泛型类操做和使用更加安全,避免引入一些存在危险隐患,形成泛型不安全,具体能够看看本文前面画的一张丑陋的漫画。因此也不得不佩服设计出这套规则语言开发者思想所折服啊。
三、最后说下,下篇文章就是泛型中的高级概念了,其实不用惧怕,只要把这篇文章概念理解清楚了后面会很简单的。
原创系列:
翻译系列:
实战系列:
欢迎关注Kotlin开发者联盟,这里有最新Kotlin技术文章,每周会不按期翻译一篇Kotlin国外技术文章。若是你也喜欢Kotlin,欢迎加入咱们~~~