Kotlin泛型
作者:刘仁鹏
参考资料:
1.https://www.yiibai.com/kotlin/generics.html
2.https://www.jianshu.com/p/832b9548b331
- 注:为更好地理解Kotlin中泛型的用法,文中会与Java的泛型机制做对比。默认读者对Java的泛型有基本了解。
1.型变的概念
-
型变分两种:协变 和 逆变。
-
F类型是C类型的父类型,我们简记为F<-C。则有:
协变 :当F<-C时,有f(F)<-f(C),则称f是协变的
逆变 : 当F<-C时,有f(C)<-f(F),则称f是逆变的 -
Java中的泛型是 不型变的,即List<String>并 不是 List<Object>的子类型。
-
为解决这个问题,Java引入了 通配符 的概念,来提供对 型变 的支持:
//List<Object> list = new ArrayList<String>(); //编译期错误:Incompatible types.
List<? extends Object> list = new ArrayList<String>(); //OK
List<? super String> list2 = new ArrayList<Object>(); //OK
-
Java中的 < ? extends T>实现了泛型的协变
-
Java中的 < ? super T>实现了泛型的逆变
-
Kotlin中没有 通配符 的概念,它通过另外两个概念实现了对 型变 的支持:声明处型变 和 类型投影 (后面详细介绍)
2.PECS
-
Java中的extends和super关键字,被用在什么场景下呢?《Effective Java》中已经给出了答案:
PECS :producer-extends, customer-super
-
PECS的原因:extends 规定了类型的上限,它被用来作为 生产者 的角色。因为它 能保证该通配符代表的具体类型,可被安全的转型为类型上限,因此作为方法的 返回类型 是安全的。而如果被当做 消费者 ,则不安全:因为它 不能保证该通配符代表的具体类型,到底是类型上限的哪个子类型,所以它不能用来作为方法的 参数类型。与此同理,super 只能被用来作为 消费者 。一个典型的CASE:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
// 校验略
ListIterator<? super T> di = dest.listIterator();
ListIterator<? extends T> si = src.listIterator();
while (si.hasNext()) {
di.next();
T element = si.next(); // src extent 生产者 作为方法返回值类型是安全的
di.set(element); // dest super 消费者 作为方法参数类型是安全的
}
}
- 注意:虽然被extends通配符修饰的对象,是 生成者 的角色,但这并不意味着该对象是 不可变的 :例如你仍然可以调用对象的clear()方法来删除对象内持有的所有元素,因为clear()根本无需任何参数。通配符 保证的唯一事情是 类型安全 ,不可变性 完全是另一回事。
3.声明处型变
- 上文提到Kotlin没有通配符的概念,而是通过另两个概念来实现型变。本小节介绍这两个概念中的 声明处型变,下一节讲解另一个概念:类型投影
- 声明处型变 是指可在泛型 被声明的地方 指定该泛型的 型变属性(协变/逆变) (Java只可在泛型 被使用的地方 制定型变属性)
- Kotlin的声明处型变通过 out修饰符 表示该泛型是 协变 的,只能被用来当做 生产者,只能出现在 输出位置。而 in修饰符 表示该泛型是 逆变 的,只能被用来当做 消费者,只能出现在 输入位置。例如:
abstract class Supplier<out T> {
abstract fun get(): T
//abstract fun set(t: T)
//编译期错误:Type parameter T is declared as 'out' but occurs in 'in' position in type T
}
abstract class Customer<in T> {
abstract fun set(t: T)
//abstract fun get(): T
//编译期错误:Type parameter T is declared as 'in' but occurs in 'out' position in type T
}
-
声明处型变 带来的 好处 是:当一个类 C 的类型参数 T 被声明为 out 时,则C<Base>可安全地作为C<Derived>的 超类。也可说:
- 类 C 在泛型 T 上是 协变的
- T 是一个协变的 类型参数
- C 是 T 的 生产者,而不是 T 的 消费者
-
out 和 in 修饰符称为 型变注解,并且由于它在类型参数 声明处 提供,所以才叫作 声明处型变
4.类型投影
- 类型投影 是Kotlin中的 使用处型变
- 类型投影 存在的必要性:声明式投影 非常方便,但 无法解决一个类既是泛型T的生产者,又是泛型T的消费者的情况,例如:
abstract class Container<T>(val size: Int) {
abstract fun get(index: Int): T
abstract fun set(index: Int, value: T)
}
- 上述Array类在泛型T上是 不型变 的,即Array<Any>和Array<Int>都不是另一个的子类型。
- 因此Kotlin仍然需要 使用处型变 的用法,即 类型投影 :被类型投影修饰的 对象,是一个 受限制的(投影的)对象,out 修饰的情况下,只可以调用其 返回类型 为类型参数T的方法;in 修饰的情况下,只可以调用其 参数类型 为类型参数T的方法。例如:
fun copy(from: Array<out Any>, to: Array<Any>) {
// 校验略
for (i in from.indices) {
to[i] = from[i]
//from[i] = null
//编译期错误:Out-projected type 'Array<out Any>' prohibits the use of 'public final operator fun set(index: Int, value: T): Unit defined in kotlin.Array
}
}
- 如上,Kotlin中虽然没有 通配符 的概念,但是 类型投影 起到了与之相同的作用:Array<out String> 对当于Java的Array<? extends String>,Array<in String> 对当于Java的Array<? super String>
5.星投影
- Kotlin中的星投影跟Java中的原始类型类似,但星投影是安全的。
- 如果类型被声明为interface Function<in T, out U>,则有以下星投影:
- Function< *, String >表示Function< in Nothing, String >
- Function< Int, * >表示Function< Int, out Any? >
- Function< *, * >表示Function< in Nothing, out Any? >
end