三、泛型型变:协变、逆变与不变
3.1 协变
3.1.1 基本定义
如果在定义的泛型类、接口和泛型方法的泛型参数前面加上 out 关键词,说明这个泛型类、接口和泛型方法是协变。
也就是说,A 是 B 的子类,那么 List<A> 也是 List<B> 的子类。
class Demo { interface Producer<out T> { // 在泛型类型形参前面指定 out 修饰符 val something: T fun produce(): T } } |
那么,协变如何解决类型不安全的问题?——只能读,不能写;只能作为方法返回值或修饰只读权限的属性,不能作为方法参数类型或可变权限的属性。
3.1.2 关键内容
- out;
- 生产者;
- 只能读,不能写;
- 对应 Java 中 Collection<? extends Object>
- 只能作为方法返回值或修饰只读权限的属性,不能作为方法参数类型;
注:Kotlin 中的 List 并不是 Java 中的 List,因为 Kotlin 中的 List 是个只读的 List,不具备修改集合中元素的操作方法。Java 中的 List 实际上相当于 Kotlin 中的 MutableList,它具有各种读和写的操作方法。
3.2 逆变
3.2.1 基本定义
如果在定义的泛型类、接口和泛型方法的泛型参数前面加上 in 关键词,说明这个泛型类、接口和泛型方法是逆变。
也就是说,A 是 B 的子类,那么 List<B> 反过来是 List<A> 的子类。
class Demo { interface Consumer<in T> { // 在泛型类型形参前面指定 in 修饰符 fun consume(value: T) } } |
那么,逆变如何解决类型不安全的问题?——只能写,不能读;只能作为方法的形参类型或修饰可变权限的属性。
3.2.2 关键内容
- in;
- 消费者;
- 只能写,不能读;
- 对应 Java 中 List<? super String>
- 只能作为方法的形参类型或修饰可变权限的属性;
3.2.3 加深理解的Demo参考如下:
正常情况下:
class Demo { private val doubleList = mutableListOf(2.0, 3.0) private val intList = mutableListOf(2, 3) private val doubleComparable = Comparator<Double> { d1, d2 -> // 一个 Double 类型比较器 d1.compareTo(d2) } private val intComparable = Comparator<Int> { i1, i2 -> // 一个 Int 类型比较器 i1.compareTo(i2) } fun test() { doubleList.sortWith(doubleComparable) intList.sortWith(intComparable) } } |
使用逆变后:
class Demo { private val doubleList = mutableListOf(2.0, 3.0) private val intList = mutableListOf(2, 3) private val numberComparable = Comparator<Number> { n1, n2 -> // 一个 Number 父类型比较器 n1.toDouble().compareTo(n2.toDouble()) } fun test() { doubleList.sortWith(numberComparable) intList.sortWith(numberComparable) } } |
3.3 不变
除去协变和逆变就是不变了,它就是我们常用的普通泛型,它既没有 in 关键字修饰,也没有 out 关键字修饰。
class Demo { interface MutableList<E> { // 没有 in 和 out 修饰 fun add(element: E) // E可以作为函数形参类型处于逆变点,输入消费 E fun subList(fromIndex: Int, toIndex: Int): MutableList<E> // E又可以作为函数返回值类型处于协变点,生产输出 E } } |
3.4 生产者、消费者的概念
Joshua Bloch 称那些你只能从中读取的对象为生产者,并称那些你只能写入的对象为消费者。他建议:“为了灵活性最大化,在表示生产者或消费者的输入参数上使用通配符类型”,并提出了以下助记符:
PECS 代表生产者-Extends、消费者-Super(Producer-Extends, Consumer-Super)。
注意:如果你使用一个生产者对象,如 List<? extends Foo>,在该对象上不允许调用 add() 或 set()。但这并不意味着该对象是不可变的:例如,没有什么阻止你调用 clear()从列表中删除所有元素,因为 clear() 根本无需任何参数。通配符(或其他类型的型变)保证的唯一的事情是类型安全。不可变性完全是另一回事。
3.5 Kotlin与Java的型变比较
不变 | 协变 | 逆变 | |
---|---|---|---|
kotlin | 实现方式:<T>,可读可写 | 实现方式:<out T>,只能读不能写,生产者 | 实现方式:<in T>,只能写不能读,消费者 |
Java | 实现方式:<T>,可读可写 | 实现方式:<? extends T>,只能读不能写,生产者 | 实现方式:<? super T>,只能写不能读,消费者 |
自创顺口溜:
泛型本是不变,Kotlin让它型变;
协变加逆变,就是没有裂变。