写在开头: 本人打算开始写一个 Kotlin 系列的教程, 一是使自己记忆和理解的更加深刻, 二是可以分享给同样想学习 Kotlin 的同学. 系列文章的知识点会以Kotlin 实战这本书中顺序编写, 在将书中知识点展示出来同时, 我也会添加对应的 Java 代码用于对比学习和更好的理解.
Kotlin 教程 (一) 基础
Kotlin 教程 (二) 函数
Kotlin 教程 (三) 类, 对象和接口
Kotlin 教程 (四) 可空性
Kotlin 教程 (五) 类型
Kotlin 教程(六)Lambda 编程
Kotlin 教程 (七) 运算符重载及其他约定
Kotlin 教程 (八) 高阶函数
泛型类型参数
泛型允许你定义带类型形参的类型, 当这种类型的实例被创建出来的时候, 类型形参被替换成称为类型实参的具体类型. 例如:
- List<String>
- Map<String, Person>
和一般类型一样, Kotlin 编译器也常常能推导出类型实参:
val authors = listOf("Dimtry", "Sevelana")
如果你想创建一个空的列表, 这样就没有任何可以推导出类型实参的线索, 你就得显式地指定它(类型形参).
- val readers: MutableList<String> = mutableListOf()
- val readers = mutableListOf<String>()
和 Java 不同, Kotlin 始终要求类型实参要么被显式地说明, 要么能被编译器推导出来. 因为泛型是 1.5 版本才引入到 Java 的, 它必须保证和基于老版本的兼容, 所以它允许使用没有类型参数的泛型类型 -- 所谓的原生态类型. 而 Kotlin 从一开始就有泛型, 所以它不支持原生态类型, 类型实参必须定义.
泛型函数和属性
如果要编写一个使用列表的函数, 希望它可以在任何列表上使用, 而不是某个具体类型的元素的列表, 需要编写一个泛型函数.
fun <T> List<T>.slice(indices: IntReange): List<T>
基本上是和 Java 的声明类似的, 在方法名前声明, 即可在函数中使用.
还可以给类或接口的方法, 顶层函数, 扩展属性以及扩展函数声明类型参数. 例如下面你这个返回列表倒数第二个元素的扩展属性:
- val <T> List<T>.penultimate: T
- get() = this[size -2]
不能声明泛型非扩展属性
普通 (非扩展) 属性不能拥有类型参数, 不能再一个类的属性中存储多个不同类型的值, 因此声明泛型非扩展函数函数没有任何意义.
声明泛型类
和 Java 一样, Kotlin 通过在类名称后面加上一对尖括号, 并把类型参数放在尖括号内来声明泛型类及泛型接口. 一旦声明之后, 就可以在类的主体内像其他类型一样使用类型参数.
- interface List<T> {
- operator fun get(index: Int): T
- }
如果你的类继承了泛型(或者实现了泛型接口), 你就得为基础类型的泛型形参提供一个类型实参.
- class StringList: List<String> {
- override fun get(index: Int): String = ...
- }
类型参数约束
类型参数约束可以限制作为 (泛型) 类和 (泛型) 函数的类型实参的类型. 如果你把一个类型指定为泛型类型形参的上界约束, 在泛型类型具体的初始化中, 其对应的类型实参就必须是这个具体类型或其子类型. 你是这样定义约束: 把冒号放在类型参数名称之后, 作为类型形参上界的类型紧随其后:
fun <T : Number> List<T>.sum(): T
相当于 Java 中的:
<T extends Number> T sum(List<T> list)
一旦指定了类型形参 T 的上界, 你就可以把类型 T 的值当做它的上界的值使用:
- fun <T : Number> oneHalf(value: T): Double {
- return value.toDouble() // 调用 Number 的方法
- }
极少数情况下, 需要在一个类型参数上指定多个约束, 这时你需要使用不同的语法:
- fun <T> ensureTrailingPeriod(seq: T)
- where T : CharSequence, T : Appendable {
- if(!seq.endWith('.') { // 调用 CharSequence 的方法
- seq.append('.')// 调用 Appendable 的方法
- }
- }
这种情况下, 可以说明作为类型实参的类型必须同时实现 CharSequence 和 Appendable 两个接口.
让类型形参非空
如果你声明的时泛型类或者泛型函数, 任何类型实参, 包括哪些可空的类型实参, 都可以替换她的类型形参. 事实上没有指定上界的类型形参将会使用 Any? 这个默认上界:
- class Processor<T> {
- fun process(value: T) {
- value?.hashCode()
- }
- }
process 函数中, 参数 value 是可空的, 尽管 T 并没有使用问号标记.
如果你想保证替换类型形参的始终是非空类型, 可以通过制定一个约束来实现. 如果你除了可空性之外没有任何限制, 可以使用 Any 代替默认的 Any? 作为上界.
- class Processor<T : Any> {
- fun process(value: T) {
- value.hashCode()
- }
- }
运行时的泛型: 擦除和实化类型参数
你可能知道, JVM 上的泛型一般是通过类型擦除实现的, 就是说泛型类实例的类型实参在运行时是不保留的.
运行时的泛型: 类型检查和转换
和 Java 一样, Kotlin 的泛型在运行时也被擦除了. 这意味着泛型类实例不会携带用于创建它的类型实参的信息. 例如, 如果你创建一个 List<String > 并将一堆字符串放到其中, 在运行时你只能看到它是一个 List, 不能识别出列表本打算包含的时哪种类型的元素.
随着擦除类型信息也带来了约束. 因为类型实参没有被存储下来, 你不能检查他们. 例如, 你不能判断一个列表是一个包含字符串的列表还是包含其他对象的列表:
- >>> if (value is List<String>)
- ERROR: Canot check for instance of erased type
那么如何检查一个值是否是列表, 而不是 set 或者其他对象. 可以使用特殊的 * 投影语法来做这样的检查:
if (value is List<*>)
这种表示拥有未知类型实参的泛型类型, 类似于 Java 中的 List<?>.
注意, 在 as 和 as? 转换中仍然可以使用一般的泛型类型. 但是如果该类有正确的基础类型但类型实参是错误的, 转换也不会失败, 因为在运行时转换发生的时候类型实参是未知的. 因此, 这样的转换会导致编译器发出 "unchecked cast" 的警告. 这仅仅是一个警告, 你仍然可以继续使用这个值.
- fun printSum(c: Collection<*>) {
- // 这里会有警告: Unchecked cast:List<*> to List<Int>
- val intList = c as? List<Int>
- ?: throw IllegalArgumentException("List is expected")
- println(intList.sum())
- }
- >>> printSum(listOf(1, 2, 3))
- 6
编译一切正常: 编译器只是发出了一个警告, 这意味着代码是合法的. 如果在一个整型的列表或者 set 上调用该函数, 一切都会如预期发生: 第一种情况会打印元素之和, 第二种情况会抛出 IllegalArgumentException 异常. 但如果你传递了一个错误类型的值, 如 List<String>, 运行时会得到一个 ClassCastException.
声明带实化类型参数的函数
前面说过, Kotlin 泛型在运行时会被擦除, 泛型函数的类型实参也是这样. 在调用泛型函数的时候, 在函数体中你不能决定调用它用的类型实参:
- >>> fun <T> isA(value: Any) = value is T
- Error: Cannot check for instance of erased type: T
通常情况下都是这样的, 只有一种例外可以避免这种限制: 内联函数. 内联函数的类型形参能够被实化, 意味着你可以在运行时引用实际的类型实参.
在之前章节中, 我们知道如果用 inline 关键字标记一个函数, 编译器会把每一次函数调用都换成函数实际的代码实现. 使用内联函数还可以提升性能, 如果该函数使用了 lambda 实参: lambda 的代码也会内联, 所以不会创建任何匿名类. 基于这种实现原理, 应该也可以想象到, 根据嵌入的上下文, 泛型在 class 文件中已经被确定了.
如果把前面例子中的 isA 函数声明成 inline 并且用 reified 标记类型参数, 你就能够用该函数检查 value 是不是 T 的实例了.
- inline fun <reified T> isA(value: Any) = value is T
- >>> println(isA<String>("abc"))
- true
- >>> println(isA<String>(123))
- false
一个实化类型参数能发挥作用的最简单的例子就是标准库函数 filterIsInstance . 这个函数接收一个集合, 选择其中哪些指定类的实例, 然后返回这些被选中的实例.
>>> val items = listOf("one", 2, "three")
- >>> println(items.filterIsInstance<String>())
- [one, three]
通过指定 < String > 作为函数的类型实参, 你表明感兴趣的只是字符串. 因此函数的返回类型是 List<String>. 这种情况下, 类型实参在运行时是已知的, 函数 filterIsInstance 使用它来检查列表中的值是不是指定为该类型实参的类的实例.
下面是 Kotlin 标准库函数 filterIsInstance 声明的简化版本:
- inline fun <reified T> Iterable<*>.filterIsInstance(): List<T> {
- val destination = mutableListOf<T>()
- for (element in this) {
- if (element is T) {
- destination.add(element)
- }
- }
- return destination
- }
在之前章节, 我们提到把函数标记成内联只有在一种情况下有性能优势, 即函数拥有函数类型的形参并且其对应的实参 lambda 和函数一起被内联的时候. 而现在我们是为了能够使用实化参数而把函数标记成内联.
为什么实化只对内联函数有效
编译器把实现内联函数的字节码插入每一次调用发生的地方. 每次你调用带实化类型参数的函数时, 编译器都知道这次特定调用中用作类型实参的切确类型. 因此, 编译器可以生成引用作为类型实参的具体类的字节码. 实际对
filterIsInstance<String>
掉用来说, 生成的代码和下面这段代码是等价的:
- for (element in this) {
- if (element is String) {
- destination.add(element)
- }
- }
因为生成的字节码引用了具体类, 而不是类型参数, 它不会被运行时发生的类型参数擦除影响.
注意, 带 reified 类型参数的 inline 函数不能再 Java 代码中调用. 普通内联函数可以像常规函数那样在 Java 中调用 -- 他们可以被调用而不能被内联. 带实化参数类型的函数需要额外的处理, 来把类型参数的值替换到字节码中, 所以他们必须永远是内联的. 这样他们不可能用 Java 那样的普通方式调用.
使用实化类型参数代替类引用
如果你是 Android 开发者, 显示 Activity 是一个最常用的方法. 也可以使用实化类型参数来代替传递作为 java.lang.Class 的 Activity 类:
- inline fun <reified T : Activity> Context.startActivity() {
- val intent = Intent(this, T::class.java)
- startActivity(intent)
- }
- >>> startActivity
::class.java 的语法展现了如何获取 java.lang.Class 对应的 Kotlin 类. 这和 Java 中的 Service.class 是完全等同的.
实化类型参数的限制
尽管实化类型参数是方便的工具, 但它们也有一些限制. 有一些事实化与生俱来的, 而另外一些则是现有的实现决定的, 而且可能在未来的 Kotlin 版本中放松这些限制.
具体来说, 可以按下面的方式使用实化类型参数:
用在类型检查和类型转换中(is,!is,as,as?)
使用 Kotlin 反射 API(::class)
获取相应的 java.lang.Class(::class.java)
作为调用其他函数的类型实参
不能做下面的这些事情:
创建指定为类型参数的类的实例
调用类型参数类的伴生对象的方法
调用带实化类型参数函数的时候使用非实化类型形参作为类型实参
把类, 属性或者非内联函数的类型参数标记成 reified
变型: 泛型和子类型化
变型的概念描述了拥有相同基础类型和不同类型实参的 (泛型) 类型之间是如何关联的: 例如, List<String > 和 List<Any > 之间如何关联.
为什么存在变型: 给函数传递实参
假如你有一个接收 List<Any > 作为实参的函数. 把 List<String > 类型的变量传给这个函数时候安全? 毫无疑问, 把一个字符串传给一个期望 Any 的函数是安全的, 因为 String 继承了 Any. 但当 String 和 Any 变成 List 接口的类型实参之后, 情况就没有这么简单了.
- fun printContents(list: List<Any>) {
- println(list.joinToString())
- }
- >>> printContents(listOf("abc", "bac"))
abc, bac
这看上去没什么问题, 我们来看另一个例子:
- fun addAnswer(list: MutableList<Any>) {
- list.add(42)
- }
- >>> val strings = mutableListOf("abc", "bac")
- >>> addAnswer(strings)
- Type mismatch. Required: MutableList<Any> Found: MutableList<String>
这个例子和上面的例子中, 区别仅仅是将 List<Any > 变成了 MutableList<Any>, 就无法将泛型为 String 的 list 传递给函数.
现在可以回答刚才那个问题了, 把一个字符串列表传给期望 Any 对象列表的函数是否安全. 如果函数添加或者替换了列表中的元素就是不安全的, 因为这样会产生类型不一致的可能性. 在 Kotlin 中, 可以通过根据列表是否可变选择合适的接口来轻松的控制. 如果函数接收的是只读列表, 可以传递具有更具体的元素类型的列表. 如果列表是可变的, 就不能这么做.
类, 类型和子类型
为了讨论类型之间的关系, 需要熟悉子类型这个术语. 任何时候如果需要的时类型 A 的值, 你都能够使用类型 B 的值(当做 A 的值), 类型 B 就称为类型 A 的子类型. 举例来说, Int 是 Number 的子类型, 但 Int 不是 String 的子类型. 这个定义还标明了任何类型都可以被认为是它自己的子类型.
术语超类型是子类型的反义词. 如果 A 是 B 的子类型, 那么 B 就是 A 的超类型.
为什么一个类型是否是另一个的子类型这么重要? 编译器在每一次给变量赋值或者给函数传递实参的时候都要做这项检查.
- fun test(i: Int) {
- val n: Number = i // 可以编译
- fun f(s: String) {/*...*/}
- f(i) // 不能编译
- }
只有值得类型是变量类型的子类型时, 才允许变量存储该值. 例如, 变量 n 的初始化器 i 的类型 Int 是变量的类型 Number 的子类型, 所以 n 的声明是合法的. 只有当表达式的类型是函数参数的类型的子类型时, 才允许把该表达式传给函数. 这个例子中 i 的类型 Int 不是函数参数的类型 String 的子类型, 所以函数 f 的调用会编译失败.
你可能认为子类型就是子类的概念, 但是为什么在 Kotlin 中称之为子类型呢? 因为, Kotlin 存在可空类型. 一个非空类型是它的可空版本的子类型, 但它们都对应着同一个类. 你始终能在可空类型的变量中存储非空类型的值, 但反过来却不行.
- var s: String = "abc"
- val t: String? = s // 编译通过
- s = t // 编译不通过
前面, 我们把 List<String > 类型的变量传给期望 List<Any > 的函数是否安全, 现在可以使用子类型化术语来重新组织: List<String > 是 List<Any > 的子类型吗? 你已经了解了为什么把
MutableList<String>
当成 MutableList<Any > 的子类型对待是不安全的. 显然, 返回来也是不成立的: MutableList<Any > 肯定不是
MutableList<String>
的子类型.
一个泛型类 (例如 MutableList) 如果对于任意两种类型 A 和 B,MutableList<A > 既不是 MutableList<B > 的子类型也不是他的超类型, 他就是被称为在该类型参数上是不变型的. Java 中所有的类都是不变型的(尽管哪些类具体的使用可以标记成可变型的, 稍后你就会看到).
List 类的类型化规则不一样, Kotlin 中的 List 接口表示的是只读集合, 如果 A 是 B 的子类型, 那么 List<A > 就是 List<B > 的子类型. 这样的类或者接口被称为协变的.
协变: 保留子类型化关系
一个协变类是一个泛型类(我们以 Producer<T > 为例), 对这种类来说, 下面的描述是成立的: 如果 A 是 B 的子类型, 那么 Producer<A > 就是 Producer<B > 的子类型. 我们说子类型化被保留了.
在 Kotlin 中, 要声明类在某个类型参数上是可以协变的, 在该类型参数的名称前面加上 out 关键字即可:
- interface Producer<out T> {
- fun produce(): T
- }
将一个类的类型参数标记为协变得, 在该类型实参没有精确匹配到函数中定义的类型形参时, 可以让该类的值作为这些函数的实参传递, 也可以作为这些函数的返回值. 例如, 想象一下有这样一个函数, 它负责喂养用类 Herd 代表的一群动物, Herd 类的类型参数确定了畜群中动物的类型.
- open class Animal {
- fun feed() {...}
- }
- class Herd<T : Animal> {
- val size: Int
- get() = ...
- operator fun get(i: Int): T {...}
- }
- fun feeAll(animals: Herd<Animal>) {
- for (i in 0 until animals.size) {
- animals[i].feed()
- }
- }
假设这段代码的用户有一群猫需要照顾:
- class Cat : Animal() {
- fun cleanLitter() {...}
- }
- fun takeCareOfCats(cats: Herd<Cat>) {
- for(i in 0 until cats.size) {
- cats[i].cleanLitter()
- // feedAll(cats) // 错误: 类型不匹配
- }
- }
如果尝试把猫群传给 feedAll 函数, 在编译期你就会得到类型不匹配的错误. 因为 Herd 类中的类型参数 T 没有用任何变型修饰符, 猫群不是畜群的子类. 可以使用显示得类型转换来绕过这个问题, 但是这种方法啰嗦, 易出错, 而且几乎从来不是解决类型不匹配问题的正确方式.
因为 Herd 类有一个类似 List 的 API, 并且不允许它的调用者添加和改变畜群中的动物, 可以把它变成协变并相应地修改调用代码.
- class Herd<out T: Animal> {
- ...
- }
你不能把任何类都变成协变得: 这样不安全. 让类在某个类型参数变为协变, 限制了该类中对该类型参数使用的可能性. 要保证类型安全, 它只能用在所谓的 out 位置, 意味着这个类只能生产类型 T 的值而不能消费它们.
在类成员的声明中类型参数的使用可以分为 in 位置和 out 位置. 考虑这样一个类, 它声明了一个类型参数 T 并包含了一个使用 T 的函数. 如果函数是把 T 当成返回类型, 我们说它在 out 位置. 这种情况下, 该函数生产类型为 T 的值. 如果 T 用作函数参数的类型, 它就在 in 位置, 这样的函数消费类型为 T 的值.
- interface Transformer<T> {
- //in 位置 //out 位置
- fun transform(t: T): T
- }
类的类型参数前的 out 关键字要求所有使用 T 的方法只能把 T 放在 out 位置而不能放在 in 位置. 这个关键字约束了使用 T 的可能性, 这保证了对应子类型关系的安全性.
重申一下, 类型参数 T 上的关键字 out 有两层含义:
子类型化被保留
T 只能用在 out 位置
现在我们看看 List<Interface > 接口. Kotlin 的 List 是只读的, 所以它只有一个返回类型为 T 的元素的方法 get, 而没有定义任何把类型为 T 的元素存储到列表中的方法. 因此, 它也是协变的.
- interface List<out T> : Collection<T> {
- operator fun get(index: Int): T
- }
注意, 类型形参不光可以直接当作参数类型或者返回类型使用, 还可以当作另一个类型的类型实参. 例如, List 接口就包含了一个返回 List<T > 的 subList 方法:
- interface List<out T> : Collection<T> {
- fun subList(fromIndex: Int, toIndex: Int): List<T>
- }
在这个例子中, 函数 subList 中的 T 也用在 out 位置.
注意, 不能把 MutableList<T > 在它的类型参数上声明成协变的, 因为它既含有接收类型为 T 的值作为参数的方法, 也含有返回这种值得方法(因此, T 出现在 in 和 out 两种位置上).
- interface MutableList<T>
- : List<T>, MultableCollection<T> {
- override fun add(element: T): Boolean
- }
编译器强制实施了这种限制. 如果这个类被声明成协变得, 编译器会报错: Type parameter T is declared as 'out' but occurs in 'in' position(类型参数 T 声明为 out 但出现在 in 位置).
注意, 构造方法的参数即不在 in 位置, 也不在 out 位置. 即使类型参数声明成了 out, 仍然可以在构造方法参数的声明中使用它:
class Herd<out T: Animal>(vararg animals: T) {...}
如果把类的实例当成一个更泛化的类型的实例使用, 变型会防止该实例被误用: 不能调用存在潜在危险的方法. 构造方法不是那种在实例创建之后还能调用的方法, 因此它不会有潜在危险.
然后, 如果你在构造方法的参数上使用了关键字 val 和 var, 同时就会声明一个 getter 和一个 setter(如果属性是可变的). 因此, 对只读属性来说, 类型参数用在了 out 位置, 而可变属性在 out 位置和 in 位置都使用了它:
class Herd<T: Animal>(var leadAnimal: T, vararg animals: T) {...}
上面这个例子中, T 不能用 out 标记, 因为类包含属性 leadAnimal 的 setter, 它在 in 位置用到了 T.
还需要注意的是, 位置规则只覆盖了类外部可见的(public,protected 和 internal)API. 私有方法的参数即不在 in 位置也不在 out 位置. 变型规则只会防止外部使用者对类的误用但不会对类自己的实现起作用:
class Herd<out T: Animal>(private var leadAnimal: T, vararg animals: T) {...}
现在可以安全地让 Herd 在 T 上协变, 因为属性 leadAnimal 变成了私有的.
逆变: 反转子类型化关系
逆变的概念可以被看成是协变的镜像: 对一个逆变来说, 它的子类型化关系与用作类型实参的类的子类型化关系是相反的. 我们从 Comparator 接口的例子开始, 这个接口定义了一个方法 compare 类, 用于比较两个给定的对象:
- interface Comparator<in T> {
- fun compare(e1: T, e2: T): Int {...}
- }
一个为特定类型的值定义的比较器显然可以比较该类型任意子类型的值. 例如, 如果有一个 Comparator<Any>, 可以用它比较任意具体类型的值.
- interface Comparator<in T> {
- fun compare(e1: T, e2: T): Int
- }
- fun main(args: Array<String>) {
- val anyComparator = Comparator<Any> { e1, e2 -> e1.hashCode() - e2.hashCode() }
- val strings = listOf("a", "b", "c")
- strings.sortedWith(anyComparator)
- }
sortedWith 函数期望一个 Comparator<String>(一个可以比较字符串的比较器), 传给它一个能比较更一般的类型的比较器是安全的. 如果你要在特定类型的对象上执行比较, 可以使用能处理该类型或者它的超类型的比较器. 这说明 Comparator<Any > 是 Comparator<String > 的子类型, 其中 Any 是 String 的超类型. 不同类型之间的子类型关系和这些类型的比较器之间的子类型化关系截然相反.
现在你已经为完整的逆变定义做好了准备. 一个在类型参数上逆变的类是这样的一个泛型类(我们以 Consumer<T > 为例), 对这种类来说, 下面的描述是成立的: 如果 B 是 A 的子类, 那么 Consumer<A > 就是 Consumer<B > 的子类型, 类型参数 A 和 B 交换了位置, 所以我们说子类型化被反转了.
in 关键字的意思是, 对应类型的值是传递进来给这个类的方法的, 并且被这些方法消费. 和协变得情况类似, 约束类型参数的使用将导致特定的子类型化关系. 在类型参数 T 上的 in 关键字意味着子类型化被反转了, 而且 T 只能用在 in 位置.
协变得, 逆变的和不变型的类
协变 | 逆变 | 不变型 |
---|---|---|
Producer<out T> | Consumer<in T> | MutableList<T> |
类的子类型化保留了:Producer<Cat> 是 Producer<Animal> 的子类型 | 子类型化反转了:Consumer<Animal> 是 Consumer<Cat> 的子类型 | 没有子类型化 |
T 只能在 out 位置 | T 只能在 in 位置 | T 可以在任何位置 |
一个类可以在一个类型参数上协变, 同时在另外一个类型参数上逆变. Function 接口就是一个经典的例子. 下面是一个单个参数的 Function 的声明:
- interface Function1<in P, out R> {
- operator fun invoke(p: P): R
- }
Kotlin 的表达发 (P) -> R 是表达 Function<P, R > 的另一种更具可读性的形式. 可以发现用 in 关键字标记的 P(参数类型) 只用在 in 位置, 而用 out 关键字标记的 R(返回类型)只用在 out 位置. 这意味着对这个函数类型的第一个类型参数来说, 子类型化反转了, 而对于第二个类型参数来说, 子类型化保留了.
fun enumerateCats(f: (Cat) -> Number) {...}
- fun Animal.getIndex(): Int = ...
- >>> enumerateCats(Animal::getIndex)
在 Kotlin 中这点代码是合法的. Animal 是 Cat 的超类型, 而 Int 是 Number 的子类型.
使用点变型: 在类型出现的地方指定变型
在类声明的时候就能够指定变型修饰符是很方便的, 因为这些修饰符会应用到所有类被使用的地方. 这被称作声明点变型. 如果你熟悉 Java 的通配符类型(? extends 和 ? super), 你会意识到 Java 用完全不同的方式处理变型. 在 Java 中, 每一次使用带类型参数的类型的时候, 还可以指定这个类型参数是否可以用他的子类型或者超类型替换. 这叫做使用点变型.
Kotlin 的声明点变型 vs. Java 通配符
声明点变形带来了更简洁的代码, 因为只用指定一次变型修饰符, 所有这个类的使用者都不用再考虑这些了, 在 Java 中, 库作者不得不一直使用通配符:
Function<? super T, ? extends R>
, 来创建按照用户期望的运行的 API. 如果你查看 Java 8 标准库的源码, 你会在每次用到 Function 接口的地方发现通配符. 例如, 下面是 Stream.map 方法的声明:
/* Java */
- public interface Stream<T> {
- <R> Stream<R> map(Function<? super T, ? extends R> mapper);
- }
Kotlin 也支持使用点变型, 允许在类型参数出现的具体位置指定变型, 即使在类型声明时它不能被声明成协变或逆变的.
你已经见过许多像 MutableList 这样的接口, 通常情况下即不是协变也不是逆变的, 因为它同时生产和消费指定为它们类型参数的类型的值. 但对于这个类型的变量来说, 在某个特定函数中只被当成其中一种角色使用的情况挺常见的: 要么是生产者要么是消费者. 例如下面这个简单的函数:
- fun <T> copyData(source: MutableList<T>,
- destination: MutableList<T>) {
- for (item in source) {
- destination.add(item)
- }
- }
这个函数从一个集合把元素拷贝到另一个集合中. 尽管两个集合都拥有不变型的类型, 来源集合只是用于读取, 而目标集合只是用于写入. 这种情况下, 集合元素的类型不需要精确匹配. 例如, 把一个字符串的集合拷贝到可以包含任意对象的集合中一点儿问题也没有.
要让这个函数支持不同类型的列表, 可以引入第二个泛型参数.
- fun <T : R, R> copyData(source: MutableList<T>,
- destination: MutableList<R>) {
- for (item in source) {
- destination.add(item)
- }
- }
- >>> val ints = mutableListOf(1, 2, 3)
- >>> val anyItems = mutableListOf<Any>()
- >>> copyData(ints, anyItems)
- >>> println(anyItems)
- [1, 2, 3]
你声明了两个泛型参数代表来源列表和目标列表中的元素类型. 为了能够把一个列表中的元素拷贝到另一个列表中, 来源元素类型应该是目标列表中的元素的子类型(Int 是 Any 的子类型).
但是 Kotlin 提供了一种更优雅的表达方式. 当函数的实现调用了那些类型参数只出现在 out 位置 (或只出现在 in 位置) 的方法时, 可以充分利用这一点, 在函数定义中给特定用途的类型参数加上变型修饰符.
- fun <T> copyData(source: MutableList<out T>,
- destination: MutableList<T>) {
- for (item in source) {
- destination.add(item)
- }
- }
可以为类型声明中类型参数任意的用法指定变型修饰符, 这些用法包括: 形参类型, 局部变量类型, 函数返回类型, 等等. 这里发生的一切被称作类型投影: 我们说 source 不是一个常规的 MutableList, 而是一个投影 (受限) 的 MutableList. 只能调用返回类型是泛型类型参数的那些方法, 或者严格的讲, 只在 out 位置使用它的方法. 编译器禁止调用使用类型参数做实参的那些方法(在 in 位置使用类型参数):
- >>> val list: MutableList<out Number> = ...
- >>> list.add(42)
- Error: Out-projected type 'MutableList<out Number>' prohibits the use of 'fun add (element: E): Boolean'
不要为使用投影类型后不能调用某些方法而吃惊, 如果需要调用那些方法, 你要用的时常规类型而不是投影. 这可能要求你声明第二个类型参数, 它依赖的原本要进行投影的类型.
当然, 实现 copyData 函数的正确方式应该是使用 List<T > 作为 source 实参的类型, 因为我们只用了声明在 List 中的方法, 并没有用到 MutableList 中的方法, 而且 List 类型参数的变型在声明时就指定了. 但这个例子对展示这个概念依然十分重要, 尤其是要记住大多数的类并没有像 List 和 MutableList 这样分开的两个接口, 一个是协变的读取接口, 另一个是不变型的读取 / 写入接口.
如果类型参数已经有 out 变型, 获取它的 out 投影没有任何意义. 就像 List<out T > 这样. 它和 List<T > 是一个意思, 因为 List 已经声明成了 class List<out T>. 编译器会发出警告, 标明这样的投影是多余的.
同理, 可以对类型参数的用法使用 in 修饰符, 来表明在这个特定的地方, 相应的值担当的时消费者, 而且类型参数可以使用它的任意子类型替换.
- fun <T> copyData(source: MutableList<T>,
- destination: MutableList<in T>) {
- for (item in source) {
- destination.add(item)
- }
- }
Kotlin 的使用点变型直接对应 Java 的限界通配符. Kotlin 中的 MutableList<out T > 和 Java 中的
MutableList<? extends T>
是一个意思. in 投影的 MutableList<in T > 对应到 Java 的
- MutableList<? super T>
- .
星号投影: 使用 * 代替类型参数
本章前面提到类型检查和转换的时候, 我们提到了一种特殊的星号投影语法, 可以用它来标明你不知道关于泛型实参的任何信息. 例如, 一个包含未知类型的元素的列表用这种语法表示为 List<*>. 现在我们深入探讨星号投影的语义.
首先需要注意的是 MutableList<*>和 MutableList<Any?>不一样. 你确信 MutableList<Any?>这种列表包含的时任意类型的元素. 而另一方面, MutableList<*>是包含某种特定类型元素的列表, 但是你不知道是哪个类型. 这种列表被创建成一个包含某种特定类型元素的列表, 比如 String, 而且创建它的代码期望只包含那种类型的元素. 因为不知道是哪个类型, 你不能像列表中写入任何东西, 因为你写入的任何值都可能会违反调用代码的期望. 但是从列表中读取元素是可行的, 因为你心里有数, 所有的存储在列表中的值都能匹配所有 Kotlin 类型的超类型 Any?:
- fun main(args: Array<String>) {
- val list: MutableList<Any?> = mutableListOf('a', 1, "qwe")
- val chars = mutableListOf('a', 'b', 'c')
- val unknownElements: MutableList<*> = if (Random().nextBoolean()) list else chars
- // unknownElements.add(42) // 编译器禁止调用这个方法
- println(unknownElements.first()) // 读取元素是安全的
- }
- // 输出
a
为什么编译器会把 MutableList<*>当成 out 投影的类型? 在这个例子的上下文中, MutableList<*>投影成了
MutableList<out Any?>
, 当你没有任何元素类型信息的时候, 读取 Any? 类型的元素任然是安全的, 但是向列表中写入元素是不安全的.
Kotlin 的 MyType<*>相当于 Java 中的 MyType<?>.
对像 Consumer<in T > 这样的逆变类型的参数来说, 星号投影等价于 < in Nothing>. 实际上, 在这种星号投影中无法调用任何签名中有 T 的方法. 如果类型参数是逆变的, 它就只能表现为一个消费者, 而且, 我们之前讨论过, 你不知道它可以消费的到底是什么. 因此, 不能让它消费任何东西.
当类型实参的信息并不重要的时候, 可以使用星号投影的语法, 不需要使用任何在签名中引用类型参数的方法, 或者只是读取数据额不关心它的具体类型. 例如, 可以实现一个接收 List<*>做参数的 printFirst 函数:
- fun printFirst(list: List<*>) {
- if (list.isNotEmpty()) {
- println(list.first())
- }
- }
来源: https://juejin.im/post/5acb22eaf265da23994ed42e