6.2 型变(Variance)

6.2.1 Java的类型通配符

Java 泛型的通配符有两种形式。我们使用

  • 子类型上界限定符? extends T 指定类型参数的上限(该类型必须是类型T或者它的子类型)
  • 超类型下界限定符? super T 指定类型参数的下限(该类型必须是类型T或者它的父类型)

我们称之为类型通配符(Type Wildcard)。默认的上界(如果没有声明)是 Any?,下界是Nothing。

代码示例:

  1. class Animal {
  2. public void act(List<? extends Animal> list) {
  3. for (Animal animal : list) {
  4. animal.eat();
  5. }
  6. }
  7. public void aboutShepherdDog(List<? super ShepherdDog> list) {
  8. System.out.println("About ShepherdDog");
  9. }
  10. public void eat() {
  11. System.out.println("Eating");
  12. }
  13. }
  14. class Dog extends Animal {}
  15. class Cat extends Animal {}
  16. class ShepherdDog extends Dog {}

我们在方法act(List<? extends Animal> list)中, 这个list可以传入以下类型的参数:

  1. List<Animal>
  2. List<Dog>
  3. List<ShepherdDog>
  4. List<Cat>

测试代码:

  1. List<Animal> list3 = new ArrayList<>();
  2. list3.add(new Dog());
  3. list3.add(new Cat());
  4. animal.act(list3);
  5. List<Dog> list4 = new ArrayList<>();
  6. list4.add(new Dog());
  7. list4.add(new Dog());
  8. animal.act(list4);
  9. List<Cat> list5 = new ArrayList<>();
  10. list5.add(new Cat());
  11. list5.add(new Cat());
  12. animal.act(list5);

为了更加简单明了说明这些类型的层次关系,我们图示如下:

对象层次类图:

6.2 型变(Variance) - 图1

集合类泛型层次类图:

6.2 型变(Variance) - 图2

也就是说,List<Dog>并不是List<Animal>的子类型,而是两种不存在父子关系的类型。

List<? extends Animal>List<Animal>List<Dog>等的父类型,对于任何的List<X>这里的X只要是Animal的子类型,那么List<? extends Animal>就是List<X>的父类型。

使用通配符List<? extends Animal>的引用, 我们不可以往这个List中添加Animal类型以及其子类型的元素:

  1. List<? extends Animal> list1 = new ArrayList<>();
  2. list1.add(new Dog());
  3. list1.add(new Animal());

这样的写法,Java编译器是不允许的。

Kotlin极简教程

因为对于set方法,编译器无法知道具体的类型,所以会拒绝这个调用。但是,如果是get方法形式的调用,则是允许的:

  1. List<? extends Animal> list1 = new ArrayList<>();
  2. List<Dog> list4 = new ArrayList<>();
  3. list4.add(new Dog());
  4. list4.add(new Dog());
  5. animal.act(list4);
  6. list1 = list4;
  7. animal.act(list1);

我们这里把引用变量List<? extends Animal> list1直接赋值List<Dog> list4, 因为编译器知道可以把返回对象转换为一个Animal类型。

相应的,? super T超类型限定符的变量类型List<? super ShepherdDog>的层次结构如下:

Kotlin极简教程

在Java中,还有一个无界通配符,即单独一个?。如List<?>?可以代表任意类型,“任意”是未知类型。例如:

  1. Pair<?>

参数替换后的Pair类有如下方法:

  1. ? getFirst()
  2. void setFirst(?)

我们可以调用getFirst方法,因为编译器可以把返回值转换为Object。
但是不能调用setFirst方法,因为编译器无法确定参数类型。

通配符在类型系统中具有重要的意义,它们为一个泛型类所指定的类型集合提供了一个有用的类型范围。泛型参数表明的是在类、接口、方法的创建中,要使用一个数据类型参数来代表将来可能会用到的一种具体的数据类型。它可以是Integer类型,也可以是String类型。我们通常把它的类型定义成 E、T 、K 、V等等。

当我们在实例化对象的时候,必须声明T具体是一个什么类型。所以当我们把T定义成一个确定的泛型数据类型,参数就只能是这种数据类型。此时,我们就用到了通配符代替指定的泛型数据类型。

如果把一个对象分为声明、使用两部分的话。泛型主要是侧重于类型的声明的代码复用,通配符则侧重于使用上的代码复用。泛型用于定义内部数据类型的参数化,通配符则用于定义使用的对象类型的参数化。

使用泛型、通配符提高了代码的复用性。同时对象的类型得到了类型安全的检查,减少了类型转换过程中的错误。

6.2.2 协变(covariant)与逆变(contravariant)

在Java中数组是协变的,下面的代码是可以正确编译运行的:

  1. Integer[] ints = new Integer[3];
  2. ints[0] = 0;
  3. ints[1] = 1;
  4. ints[2] = 2;
  5. Number[] numbers = new Number[3];
  6. numbers = ints;
  7. for (Number n : numbers) {
  8. System.out.println(n);
  9. }

在Java中,因为 Integer 是 Number 的子类型,数组类型 Integer[] 也是 Number[] 的子类型,因此在任何需要 Number[] 值的地方都可以提供一个 Integer[] 值。

而另一方面,泛型不是协变的。也就是说, List<Integer> 不是 List<Number> 的子类型,试图在要求 List<Number> 的位置提供 List<Integer> 是一个类型错误。下面的代码,编译器是会直接报错的:

  1. List<Integer> integerList = new ArrayList<>();
  2. integerList.add(0);
  3. integerList.add(1);
  4. integerList.add(2);
  5. List<Number> numberList = new ArrayList<>();
  6. numberList = integerList;

编译器报错提示如下:

Kotlin极简教程

Java中泛型和数组的不同行为,的确引起了许多混乱。

就算我们使用通配符,这样写:

  1. List<? extends Number> list = new ArrayList<Number>();
  2. list.add(new Integer(1)); //error

仍然是报错的:

Kotlin极简教程

为什么Number的对象可以由Integer实例化,而ArrayList<Number>的对象却不能由ArrayList<Integer>实例化?list中的<? extends Number>声明其元素是Number或Number的派生类,为什么不能add Integer?为了解决这些问题,需要了解Java中的逆变和协变以及泛型中通配符用法。

逆变与协变

Animal类型(简记为F, Father)是Dog类型(简记为C, Child)的父类型,我们把这种父子类型关系简记为F <| C。

而List<Animal>, List<Dog>的类型,我们分别简记为f(F), f(C)。

那么我们可以这么来描述协变和逆变:

当F <| C 时, 如果有f(F) <| f(C),那么f叫做协变(Convariant);
当F <| C 时, 如果有f(C) <| f(F),那么f叫做逆变(Contravariance)。
如果上面两种关系都不成立则叫做不可变。

协变和逆协变都是类型安全的。

Java中泛型是不变的,可有时需要实现逆变与协变,怎么办呢?这时就需要使用我们上面讲的通配符?

<? extends T>实现了泛型的协变

  1. List<? extends Number> list = new ArrayList<>();

这里的? extends Number表示的是Number类或其子类,我们简记为C。

这里C <| Number,这个关系成立:List<C> <| List< Number >。即有:

  1. List<? extends Number> list1 = new ArrayList<Integer>();
  2. List<? extends Number> list2 = new ArrayList<Float>();

但是这里不能向list1、list2添加除null以外的任意对象。

  1. list1.add(null);
  2. list2.add(null);
  3. list1.add(new Integer(1)); // error
  4. list2.add(new Float(1.1f)); // error

因为,List<Integer>可以添加Interger及其子类,List<Float>可以添加Float及其子类,List<Integer>、List<Float>都是List<? extends Number>的子类型,如果能将Float的子类添加到List<? extends Number>中,那么也能将Integer的子类添加到List<? extends Number>中, 那么这时候List<? extends Number>里面将会持有各种Number子类型的对象(Byte,Integer,Float,Double等等)。Java为了保护其类型一致,禁止向List<? extends Number>添加任意对象,不过可以添加null。

Kotlin极简教程

<? super T>实现了泛型的逆变

  1. List<? super Number> list = new ArrayList<>();

? super Number 通配符则表示的类型下界为Number。即这里的父类型F是? super Number, 子类型C是Number。即当F <| C , 有f(C) <| f(F) , 这就是逆变。代码示例:

  1. List<? super Number> list3 = new ArrayList<Number>();
  2. List<? super Number> list4 = new ArrayList<Object>();
  3. list3.add(new Integer(3));
  4. list4.add(new Integer(4));

也就是说,我们不能往List<? super Number >中添加Number的任意父类对象。但是可以向List<? super Number >添加Number及其子类对象。

PECS

现在问题来了:我们什么时候用extends什么时候用super呢?《Effective Java》给出了答案:

PECS: producer-extends, consumer-super

比如,一个简单的Stack API:

  1. public class Stack<E>{
  2. public Stack();
  3. public void push(E e):
  4. public E pop();
  5. public boolean isEmpty();
  6. }

要实现pushAll(Iterable<E> src)方法,将src的元素逐一入栈:

  1. public void pushAll(Iterable<E> src){
  2. for(E e : src)
  3. push(e)
  4. }

假设有一个实例化Stack<Number>的对象stack,src有Iterable<Integer>与 Iterable<Float>;

在调用pushAll方法时会发生type mismatch错误,因为Java中泛型是不可变的,Iterable<Integer>与 Iterable<Float>都不是Iterable<Number>的子类型。

因此,应改为

  1. // Wildcard type for parameter that serves as an E producer
  2. public void pushAll(Iterable<? extends E> src) {
  3. for (E e : src) // out T, 从src中读取数据,producer-extends
  4. push(e);
  5. }

要实现popAll(Collection<E> dst)方法,将Stack中的元素依次取出add到dst中,如果不用通配符实现:

  1. // popAll method without wildcard type - deficient!
  2. public void popAll(Collection<E> dst) {
  3. while (!isEmpty())
  4. dst.add(pop());
  5. }

同样地,假设有一个实例化Stack<Number>的对象stack,dst为Collection<Object>;

调用popAll方法是会发生type mismatch错误,因为Collection<Object>不是Collection<Number>的子类型。

因而,应改为:

  1. // Wildcard type for parameter that serves as an E consumer
  2. public void popAll(Collection<? super E> dst) {
  3. while (!isEmpty())
  4. dst.add(pop()); // in T, 向dst中写入数据, consumer-super
  5. }

Naftalin与Wadler将PECS称为 Get and Put Principle

java.util.Collectionscopy方法中(JDK1.7)完美地诠释了PECS:

  1. public static <T> void copy(List<? super T> dest, List<? extends T> src) {
  2. int srcSize = src.size();
  3. if (srcSize > dest.size())
  4. throw new IndexOutOfBoundsException("Source does not fit in dest");
  5. if (srcSize < COPY_THRESHOLD ||
  6. (src instanceof RandomAccess && dest instanceof RandomAccess)) {
  7. for (int i=0; i<srcSize; i++)
  8. dest.set(i, src.get(i));
  9. } else {
  10. ListIterator<? super T> di=dest.listIterator(); // in T, 写入dest数据
  11. ListIterator<? extends T> si=src.listIterator(); // out T, 读取src数据
  12. for (int i=0; i<srcSize; i++) {
  13. di.next();
  14. di.set(si.next());
  15. }
  16. }
  17. }