变异
Java中比较诡异的一部分内容就是通配符类型。通配符在Kotlin中并不支持,Kotlin提出了两个不同的概念:声明变异(declaration-site variance)和类型预测(type projections)。
首先,我们想想为什么Java需要这样的通配符。这个问题在Effective Java中进行解释了。
泛型类型在Java中是不变的,这意味着List<String>并不是List<Object>的子类型。为什么会这样呢?如果List的泛型类型是可变的,那么这对于Java数组来说无疑是最好的了,因为下面的代码将会编译通过,但是会有一个运行时异常。
// Java
List<String> strs = new ArrayList<String>();
List<Object> objs = strs;
objs.add(1);
String s = strs.get(0);
因此Java不允许这么做来保证运行时安全。但是这种特性还是有一定的影响。比如Collection的addAll()方法。
// Java
interface Collection<E> ... {
void addAll(Collection<E> items);
}
但是我们不能做以下的简单操作,尽管是安全的。
// Java
void copyAll(Collection<Object> to, Collection<String> from) {
to.addAll(from);
}
所以这就是为什么Collection的addAll()方法签名是如下方式:
// Java
interface Collection<E> ... {
void addAll(Collection<? extends E> items);
}
通配符类型表达式? extends T意味着这个方法接收T对象子类的集合,而不是T类型的集合。这就意味着我们可以安全的读取T类型的子类的单个元素,但是不能进行写操作,因为我们不知道具体的T的子类类型是什么。因为这种限制,但是我们期待这样的行为:Collection<String>是Collection<? extends Object>的子类。换句话说通配符与extends界定了上限,这导致了一种共变。
理解这种小技巧的核心很简单:如果你仅仅是从集合中取出元素,那么使用String的集合,在读取时使用Object将是可以的;如果你仅仅是从集合中插入元素,如果使用Object的集合,那么将里面插入String类型的元素也是可以的。在Java中List<? super String>是List<Object>的超类。
后者被称为逆变性,在List<? super String>中你仅仅可以调用使用String作为参数的方法(add(String)、set(int, String)),如果你调用的方法返回List<T>中的T,你不会得到一个String,而是Object。
Joshua Bloch将只能读取的对象称为生产者,将只能写入的对象为消费者。他建议:为了最大的灵活性,使用通配符在输入参数中来代表生产者或消费者。
PECS代表生产者-Extends,消费者-Super。
请注意:如果你使用一个生产者对象,List<? extends Foo>,你不能调用add()或set()方法,但是这并不意味着这个对象是不可变的:比如,没有什么可以阻止你调用clear()方法来删除所有的元素,因为clear()方法不需要传入任何的参数。通配符唯一的保证就是类型安全。不变形则是完全另一回事。