我们先来看一个导出类型的数组赋予基类型数组的例子:

public class Fruit {}
public class Apple extends Fruit {}
public class RedApple extends Apple {}
public class CovariantArrays {

    public static void main(String[] args) {
        Fruit[] fruits = new Apple[10];
        fruits[0] = new Apple();
        System.out.println("成功存入 Apple");
        fruits[1] = new RedApple();
        System.out.println("成功存入 RedApple");
        fruits[2] = new Fruit();
        System.out.println("成功存入 Fruit");
    }

}
成功存入 Apple
成功存入 RedApple
Exception in thread "main" java.lang.ArrayStoreException: mtn.baymax.charpter15.Fruit
	at mtn.baymax.charpter15.CovariantArrays.main(CovariantArrays.java:16)

上述代码虽然能通过编译,但在运行时会抛出 ArrayStoreException 表示我们将错误的类型存储至数组中。

fruits 数组的实际引用仍然是 Apple 类型的数组,编译器虽然允许你将任何 Fruit 类型及其导出类放入数组中,但数组存储的元素类型是不可变的,只能存放 Apple 类型或者及其子类,故 fruits 中不能存储 Fruit 类型。

实际上,数组的向上转型在这是不合适的。但可怕的是,即使存储类型不对,也能通过编译,我们只能在运行期间发现这个错误。

但有时候我们希望一个存储 Fruit 类型的容器来接收各类存储 Fruit 及 Fruit 导出类的容器,这时该怎么办呢?

我们首先想到用 List 来作为容器,List 会对存入的元素做类型检查,加强代码的安全性。

不过持有 Apple 类型的 List 仍然不等于持有 Fruit 类型的 List 。这是因为 Fruit 类型的 List 可以存储 Fruit 及其任何导出类,但 Apple 类型的 List 只能存储 Apple 类型及其子类,这两种容器本质是不同的(想象一下,你能在 Apple 类型的 List 中放入 Orange 类型嘛?)。故下述代码是无法通过编译的。

public class GenericsAndCovariance {

    public void printFruit(ArrayList<Fruit> fruits) {
        for (Fruit fruit : fruits) {
            System.out.println(fruit);
        }
    }

    public static void main(String[] args) {
        ArrayList<Apple> apples = new ArrayList<>();
        GenericsAndCovariance gc = new GenericsAndCovariance();
        //类型错误,无法编译
        //gc.printFruit(apples);
    }

}

想要 fruits 能接收持有任何 Fruit 导出类型的 List ,使用泛型的通配符便可以解决。

public class GenericsAndCovariance {

    public void printFruit(ArrayList<? extends Fruit> fruits) {
        for (Fruit fruit : fruits) {
            System.out.println(fruit);
        }
    }

    public static void main(String[] args) {
        ArrayList<Apple> apples = new ArrayList<>();
        apples.add(new Apple());
        apples.add(new Apple());
        apples.add(new Apple());
        GenericsAndCovariance gc = new GenericsAndCovariance();
        gc.printFruit(apples);
        ArrayList<Fruit> fruits = new ArrayList<>();
        fruits.add(new Fruit());
        fruits.add(new Fruit());
        fruits.add(new Fruit());
        gc.printFruit(fruits);
    }

}
mtn.baymax.charpter15.Apple@1b6d3586
mtn.baymax.charpter15.Apple@4554617c
mtn.baymax.charpter15.Apple@74a14482
mtn.baymax.charpter15.Fruit@1540e19d
mtn.baymax.charpter15.Fruit@677327b6
mtn.baymax.charpter15.Fruit@14ae5a5

ArrayList<? extends Fruit> fruits 表示持有任何继承自 Fruit 类型的 List,故 fruits 可以接收持有 Frult 和 Apple 类型的 List。

虽然我们可以正常读取 fruits 中存储的元素,但我们不能往 fruits 中存放任何元素,哪怕 Object 也不行,这又是为什么呢?

public void printFruit(ArrayList<? extends Fruit> fruits) {
        for (Fruit fruit : fruits) {
            System.out.println(fruit);
        }
        //下面三种类型均无法通过编译
        //fruits.add(new Fruit());
        //fruits.add(new Apple());
        //fruits.add(new Object());
    }

当任何持有 Fruit 导出类的 List 被赋值给 fruits 时,都会执行向上转型(如同第一个例子中数组向上转型),不过这里向上转型的类型并不是某个具体类型,我们只知道它继承自 Fruit,可能是 Fruit 、Apple,还有可能是 Orange、Banana 。在无法确定 List 容器中具体类型的情况下,编译器禁止我们往容器中添加任何数据,但因知道存储的类型都继承自 Fruit,故在读取的时候,可以安全的向上转型为 Fruit。