假设我们有以下的类别层次:

Layer1 --> Layer2 --> Layer3 --> Layer4

其中Layer1是位于最高位置的基类,Layer2Layer1的直接子类,Layer3又是Layer2的直接子类,等等.

我们在使用数组时会有这样的用法:

Layer1[] layerArray=new Layer2[10];

此时尽管运行时可能产生某些错误,例如往一个明明是Layer2的数组中加入一个Layer1的实例,但编译期并不会有什么问题.这是因为Java的数组存在称之为协变的现象,即如果Layer2Layer1的子类,那么Layer2的数组也是Layer1的数组的子类.我们可以这样来验证:

Layer2[] layer2Array=new Layer2[10];

System.out.println(layer2Array instanceof Layer1[]);

程序输出的结果为true.

Layer1[] layerArray=new Layer2[10];这样写有一个很大的缺点,那就是你可以写

layerArray[0]=new Layer1();

这样的代码,编译器不会报错,但实际上我们往一个Layer2的数组里塞了一个Layer1的实例,这在运行时会扔出一个java.lang.ArrayStoreException。即是说数组没有提供编译期的类型检查。

而容器(例如List)很容易被我们认为是另一种数组,只是能够容纳各种类型以及动态改变大小而已。在JavaSE5之前,即使这样认为也不会给编程带来麻烦,因为容器没有办法指定所容纳的具体类型。但在引入泛型之后,我们可能为了编译期的类型检查以及动态改变大小两个理由而迫不及待的用容器彻底代替数组,也就有可能想当然的写出这样的代码:

List<Layer1> list=new ArrayList<Layer2>();

这样写的理由很简单,总是希望用基类型的引用来提供灵活性。然而遗憾的是,上述代码编译不能通过(或许编译期就不能通过这一点,反而是好事),原因就在于容器没有“协变”现象,一个Layer2List并不是Layer1List的子类,而是完全不同的类。因此上面的代码同

Integer i=new String("你好");

一样荒谬,自然逃不过编译器的法眼。

难道就不能用泛型创建这样一种List的引用,类型参数只使用基类型,而在实例化的时候可以使用子类型,但又可以借助泛型的编译期检查么?

例如我想要一个List<Layer1>类型的引用,这个引用可以指向ArrayList<Layer2>ArrayList<Layer4>(因为我不想关心实例化时的具体类型),但是一旦实例化一个ArrayList<Layer2>以后,又能借助编译器来检查向其中添加的确实是Layer2的实例,而非StringInteger。这能不能办到呢?

答案是不能,虽然初试之下,我们可能会利用泛型通配符想当然的写出如下代码:

List<? extends Layer1> layer1List=new ArrayList<Layer2>();

乍看之下,代码的本意是要建立一个泛型List的引用,而泛型的类别参数是任何Layer1的子类均可,这不正好合我们刚才所说的意思吗?而且这回编译器也没有报错,应该OK了吧!

接下来的事情却让人哭笑不得,这个List竟然不能添加任何元素,无论是写

layer1List.add(new Layer1());

还是写

layer1List.add(new Layer2());

均报错(编译期即错,不用待到运行时)。甚至是

layer1List.add(new Object());

也报错(这证明了无论提供的类型参数是Layer1的子类还是超类,亦或者Layer1本身,均报错)。

这是为什么呢?(给读者3分钟思考,然后带着诡异的微笑揭晓答案)

原来extends关键字圈定的是泛型参数的上界,回头单看

List<? extends Layer1>

这一句,其实说的是,我有一个泛型的List,它的类别参数是Layer1的一个子类,因此如果这个子类是Layer3,那么Layer2以上的类别就不能向该List中添加(这个没什么问题吧,别绕进去了哦),如果这个子类别是Layer4,那么Layer3以上的类别就不能向该List中添加(想象我们的类别体系还存在Layer5,Layer6等等,显然子类别是哪一个都有可能,那就是说拒绝任何一个类别都是说得通的)。因此编译器根本无法确定这个List到底可以放什么不可以放什么,所以统统拒绝。

更正式一点说,extends给了一个上界,只有该界以下的类别才能合法的添加到List中,但是这个上界本身都是不确定的(它可以无限往类别体系的下方移动),自然也就说不出哪些类别是合法的了。

结论:想要容器提供类似于数组那样的协变效果,而又要有类型检查,至少在我所知范围,办不到。