为什么Java不允许创建范型数组

问题示例

List<Integer>[] intListArr = new ArrayList<Integer>[8]; // 编译时报错

能看到这么看似没啥问题的一个简单语句甚至连编译都不会通过,为了能理解这里面的缘由,我们先得了解变型(Variant)的概念

变型(variant)

  • 协变(covariant) 允许在子类出现的地方用超类替换(A feature which allows to substitute a subtype with supertype.)
  • 逆变(contravariant) 允许在超类出现的地方用子类替换(A feature which allows to substitute a supertype with subtype.)
  • 不变(invariant) 以上两种都不适用。

数组中的变型

早期版本的Java不包含泛型(generics,即参数化多态)。在这样的设置下,使数组为“不变”将导致许多有用的多态程序被排除。
例如,考虑一个用于重排(shuffle)数组的函数,或者测试两个数组相等的函数,使用Object的equals方法. 函数的实现并不依赖于数组元素的确切类型,因此可以写一个单独的实现而适用于所有的数组:

boolean equalArrays (Object[] a1, Object[] a2);
void shuffleArray(Object[] a);

然而,如果数组类型被处理为“不变”,那么它仅能用于确切为Object[]类型的数组。对于字符串数组等就不能做重排操作了。

所以,Java把数组类型处理为协变。在Java中,String[]是Object[]的子类型。

范型中的变型

当范型最初在jdk1.5被引入的时候,是有意没有把它设计成协变的,我们来看个例子:

List<Dog> dogs = new ArrayList<Dog>(); // Dog的超类是Animal
List<Animal> animals = dogs; // 把dogs的引用赋给animals超类,编译不通过
animals.add(new Cat()); // Cat是超类Animal的另一个实现
Dog dog = dogs.get(0); // 看起来这应该是对的?^^

即使jdk团队修改底层实现让这段程序能通过编译,最终的结果肯定是ClassCastException,这时候我们再来看看范型设计的目的:
Generics add stability to your code by making more of your bugs detectable at compile time. 

范型希望能在编译期间就能找到并避免程序潜在的bug,所以根本就不会把范型实现为协变,这样会更加容易出错,也就是我们在很多书中看到对本问题的解释:这就违背了 Java 泛型的设计原则

可能有人会问,通配符不是让范型可以协变或逆变或不变吗?

补充说明:
通配符的能力是可以通过指定一个更严格的界来让范型变得更加特化,使范型可以在上界协变(List<? extends Cat> 是 List<? extends Animal>的子类型)以及在下界逆变。这是一种范型定义的描述,并不是实际使用时对范型的类型实例化,所以通配符丰富了范型的功能,和范型本身是不变(invariant)的特性不冲突。

结论

说了这么多,其实就是想说明一种设计的初衷,数组是协变的,范型是不变的,这两者结合在一起使用那就肯定会出问题,例如:

List<Integer>[] intListArr = new ArrayList<Integer>[8];// 假设范型数组创建成功
List<Long> longList = new ArrayList<Long>();
List<Number>[] numberArr = intListArr; // 协变性保证可以赋值
numberArr[0] = longList;
Integer value = numberArr[0].get(0);

结果就是ClassCastException了。

当然如果代码写成new ArrayList[8]编译是通过了,这时候范型是对变量intListArr的类型说明,而不是创建范型数组了。

参考

posted @ 2020-06-14 22:15  Mr靖哥哥  阅读(318)  评论(0编辑  收藏  举报