Kotlin 泛型中的 in 和 out

Java基础

浏览数:289

2019-8-22

协变

在 Java 的泛型系统中. 泛型默认是不支持协变(covariant). 也就是说在 Java 中. 即使 A 是 B 的子类, List<A> 也不是 List<B> 的子类.
协变在某些情况下会造成一些问题, 比如在 Java 中, 数组是支持协变的. 下面的代码可以正常通过编译, 只有在运行时才会报错.

// snippet 1
// 数组是协变的, String[] 是 Object 的子类
Object [] objects = new String[10]
objects[1] = 1

虽然协变会引起一些不能在编译时就发现的明显错误, 但是在 Java 一直到 Java 5 才引入了泛型. 如果数组不支持协变, 很多方法就没有办法实现了. 比如 Arrays.sort(Ojbect[] array)这个方法. 不知持协变的情况下只能对每一个不同对象都实现一个方法.
在有了泛型的支持后, 数组协变这个特性就变成了一个彻彻底底的设计缺陷. 但是为了保持向下的兼容, Java 还是一直保留这个设计.
Kotlin 并没有类似 Java 的兼容包袱, 所以 Kotlin 中的 Array 是不支持协变的.下面的代码不能通过编译.

// snippet 2
val objects: Array<Object> = Array<String>(1) { "" } // Error

协变的作用

在一些情况下, 协变依然很有用. 比如我们实现一个List 的 copy 的函数如下:

// snippet 3
// 从 src 复制元素到 
static <T> void copy(List<T> dest, List<T> src)

当需要将一个类型为 List<Integer> 的 src 复制到 List<Number> dest 我们可能会写出如下的代码:

// snippet 4
List<Number> dest = ... // 初始化 dest
List<Integer> src = ...// 初始化 src
copy(dest, src)

但是 List<Integer> 并不是 List<Number> 的子类. 所以上面的代码并不能通过编译.
Java 提供了有限通配符类型 (bounded wildcard type) 来处理这种情况.上面的代码可以被改写成下面的形式:

// snippet 5
static <T> void copy(List<T> dest, List<? extend T> src)

这样改写后, src 的类型就变成 T 的某一个子类的列表. 也就是说 src 是支持协变的.

逆变

逆变(contravariant) 跟协变相反. 如果 List 支持逆变, 且 A 是 B 的子类, 那么 List<B> 是 List<A> 是子类.
逆变在函数中比较常见, 比如在 Kotlin 中 (Any) -> () 是 (String) -> () 的子类. Java 也通过 wildcard type 的方式支持了逆变. 声明方式如下:

List<? super Number> a;

变量 a 可以接受 List<Integer>, List<Double> 等类型.

PECS 法则

在函数的接口里使用通配符类型可以提高接口的灵活度. 但是参数究竟要使用 super 还是 extend 呢. Effective Java 中给出了一个 PECS 法则.

PSCS stands for producers-extends, consumer-super

也就是说, 如果一个参数是起的是生产者的作用, 那应该用 extend, 如果起的是消费者的作用, 就应该使用 super. 一个最好的例子就是 JDK 里 Collections 中的 copy 方法. 函数定义如下:

public static <T> void copy(List<? super T> dest, List<? extend T> src)

该函数里 src 提供需要复制的内容, 是一个生产者. 所以使用了 extend. dest 接受复制过来的元素, 是一个消费者. 所以使用的是 super. 判读一个参数是生产者还是消费者需要一定的编码的经验. 下面是我的两条经验:

  • 如果是参数是一个容器类型, 比如 List. 那么如果你在函数中是只是读取容器内容, 那么该参数就是一个生产者. 反正, 如果只是往容器里添加和删除, 而没有读取那么这个参数就是消费者.
  • 如果参数是一个 function Object, 可以将其改成 Lambda 函数. 正常在 Lambda 里当作参数的类型参数要求是逆变的, 返回值是协变的 比如 Comparator<T> 改写成 Lambda 形式应该是 (T, T) ->Int, 这里的 T 是只出现在参数中, 应该是逆变的. 当函数需要 Comparator 作为参数时通常使用会使用 super. 比如 Collections 中 sort 函数的定义为:
public static <T> void sort(List<T> list, Comparator<? super T> c)

Kotlin 中的 in 和 out

Kotlin 中可以声明泛型类型是协变还是逆变的. out 修饰类型参数是协变的, in 修饰的类型参数支持逆变.
比如 Collections 的 copy 方法的可以定义为:

public <T: Any> fun copy(dest: List<in T>, src: List<out T>)

除了在参数中使用外. 还可以直接在类型中使用. 比如 Kotlin 中的 List 是不可变的. 所以 List 定义里直接声明为协变的.

public interface List<out E>: Collection<E>

in 和 out 两个关键字应该是取自: Consumer in, Producer out!

参考资料:

Kotlin Generics: https://kotlinlang.org/docs/reference/generics.html

作者:zoro_x