6.3 Kotlin的泛型特色

正如上文所讲的,在 Java 泛型里,有通配符这种东西,我们要用? extends T指定类型参数的上限,用 ? super T指定类型参数的下限。

而Kotlin 抛弃了这个东西,引用了生产者和消费者的概念。也就是我们前面讲到的PECS。生产者就是我们去读取数据的对象,消费者则是我们要写入数据的对象。这两个概念理解起来有点绕。

我们用代码示例简单讲解一下:

  1. public static <T> void copy(List<? super T> dest, List<? extends T> src) {
  2. ...
  3. ListIterator<? super T> di = dest.listIterator(); // in T, 写入dest数据
  4. ListIterator<? extends T> si = src.listIterator(); // out T, 读取src数据
  5. ...
  6. }

List<? super T> dest是消费数据的对象,这些数据会写入到该对象中,这些数据该对象被“吃掉”了(Kotlin中叫in T)。

List<? extends T> src是生产提供数据的对象。这些数据哪里来的呢?就是通过src读取获得的(Kotlin中叫out T)。

6.3.1 out Tin T

在Kotlin中,我们把那些只能保证读取数据时类型安全的对象叫做生产者,用 out T标记;把那些只能保证写入数据安全时类型安全的对象叫做消费者,用 in T标记。

如果你觉得太晦涩难懂,就这么记吧:

out T 等价于? extends T
in T 等价于 ? super T
此外, 还有 * 等价于?

6.3.2 声明处型变

Kotlin 泛型中添加了声明处型变。看下面的例子:

  1. interface Source<out T> {
  2. fun <T> nextT();
  3. }

我们在接口的声明处用 out T 做了生产者声明以实现安全的类型协变:

  1. fun demo(str: Source<String>) {
  2. val obj: Source<Any> = str // 合法的类型协变
  3. }

Kotlin 中有大量的声明处协变,比如 Iterable 接口的声明:

  1. public interface Iterable<out T> {
  2. public operator fun iterator(): Iterator<T>
  3. }

因为 Collection 接口和 Map 接口都继承了 Iterable 接口,而 Iterable 接口被声明为生产者接口,所以所有的 Collection 和 Map 对象都可以实现安全的类型协变:

  1. val c: List<Number> = listOf(1, 2, 3)

这里的 listOf() 函数返回 List<Int>类型,因为 List 接口实现了安全的类型协变,所以可以安全地把 List<Int>类型赋给 List<Number> 类型变量。

6.3.3 类型投影

将类型参数 T 声明为 out 非常方便,并且能避免使用处子类型化的麻烦,但是有些类实际上不能限制为只返回 T

一个很好的例子是 Array:

  1. class Array<T>(val size: Int) {
  2. fun get(index: Int): T { }
  3. fun set(index: Int, value: T) { }
  4. }

该类在 T 上既不能是协变的也不能是逆变的。这造成了一些不灵活性。考虑下述函数:

  1. fun copy(from: Array<Any>, to: Array<Any>) {
  2. assert(from.size == to.size)
  3. for (i in from.indices)
  4. to[i] = from[i]
  5. }

这个函数应该将项目从一个数组复制到另一个数组。如果我们采用如下方式使用这个函数:

  1. val ints: Array<Int> = arrayOf(1, 2, 3)
  2. val any = Array<Any>(3) { "" }
  3. copy(ints, any) // 错误:期望 (Array<Any>, Array<Any>)

这里我们将遇到同样的问题:Array <T>T 上是不型变的,因此 Array <Int>Array <Any> 都不是另一个的子类型。

那么,我们唯一要确保的是 copy() 不会做任何坏事。我们阻止它写到 from,我们可以:

  1. fun copy(from: Array<out Any>, to: Array<Any>) {}

现在这个from是一个受Array<out Any>限制的(投影的)数组。在Kotlin中,称为类型投影(type projection)。其主要作用是参数作限定,避免不安全操作。

类似的,我们也可以使用 in 投影一个类型:

  1. fun fill(dest: Array<in String>, value: String) {}

Array<in String> 对应于 Java 的 Array<? super String>,也就是说,我们可以传递一个 CharSequence 数组或一个 Object 数组给 fill() 函数。

类似Java中的无界类型通配符?, Kotlin 也有对应的星投影语法*

例如,如果类型被声明为 interface Function <in T, out U>,我们有以下星投影:

  • Function<*, String> 表示 Function<in Nothing, String>
  • Function<Int, *> 表示 Function<Int, out Any?>
  • Function<*, *> 表示 Function<in Nothing, out Any?>

*投影跟 Java 的原始类型类似,不过是安全的。