前言
在 Java 中,泛型提供了一种在编译时期检查和强制执行类型安全的机制,以增加代码的可读性和重用性。
但是在使用泛型中我们会发现 Java 数组不支持创建泛型数组,如下代码创建一个泛型数组
Java
List<String>[] strList = new ArrayList<String>[10];//编译错误,非法创建
编译器会报错提示: Cannot create a generic array of ArrayList
接下来分析一下为什么 Java 数组为什么不支持泛型。
1、什么是泛型?
Java 泛型(generics)是 JDK 5 中引入的一个新特性。好处是在编译时能够检查类型安全性,提高代码的重用性。看下面的 demo:
Java
// 1、在编译时能够检查类型安全性。
ArrayList list1 = new ArrayList();
list1.add("abcdefg");
list1.add(123456);
ArrayList<String> list2 = new ArrayList<>();
list2.add("abcdefg");
list2.add(123456);//编译错误
//2、提高重用性
private static float add(float a, float b) {
return a + b;
}
private static double add(double a, double b) {
return a + b;
}
//泛型只需要一个方法
private static <T extends Number> double add(T a, T b) {
return a.doubleValue() + b.doubleValue();
}
泛型的本质是参数化类型,即给类型指定一个参数,然后在使用时再指定此参数具体的值,那样这个类型就可以在使用时决定了。
2、泛型数组的类型安全
Java
List<String>[] strList = new ArrayList<String>[10];//编译错误,非法创建
List<String>[] strList2 = new ArrayList[10];//ok
strList2[0] = new ArrayList<String>();//ok
strList2[1] = "";//编译错误
strList2[2] = new ArrayList<Integer>();//编译错误
从上面代码可以发现,编译器不允许创建带泛型的数组实例,但允许数组引用类型中出现泛型。
我们尝试往数组strList2
赋值一些元素,第一个我们new ArrayList<String>
,把它放进数组没有问题。第二和第三个会提示编译错误,编译器会告诉我们它所期望的类型是List<String>
。所以在这个数组里元素只能是List
,并且它的泛型是String
这样的元素才能够通过编译。
所以我们可以发现,对于数组的引用类型,也就是等号左边上的范型来说,作用是在编译期去保证数组的类型安全。
数组的协变关系
scala
class Fruit{ }
class Apple extends Fruit{ }
class Orange extends Fruit{ }
Fruit[] array = new Apple[10];
if (array instanceof Apple[]) {//true
System.out.println("array instanceof Apple[]");
}
if (array instanceof Orange[]) {//false
System.out.println("array instanceof Orange[]");
}
if (array instanceof Fruit[]) {//true
System.out.println("array instanceof Fruit[]");
}
if (array instanceof Object[]) {//true
System.out.println("array instanceof Object[]");
}
上述代码我们的引用是 Fruit 类型的数组 array,但是我们创建的却是 Apple 类型的数组。但是编译器是能够通过编译的。说明 Fruit 如果是 Apple 的父类的话,那么 Fruit 类型的数组就同样是 Apple 类型数组的父类型。
运行上述代码我们可以发现,除了 Orange 类型的数组以外,其他的都是能够返回 true。
这是因为 Java 中的数组是协变的,Java 中的协变是指一个数据类型能够自动转换为其子类型,这意味着数组类型的继承关系与其元素类型的继承关系是一致的。
3、泛型列表的类型安全
Java
ArrayList<Fruit> fruits = new ArrayList<Apple>();//编译
ArrayList<? extends Fruit> fruits2 = new ArrayList<Apple>();//支持协变的形式 ok
对于泛型集合来说,我们以ArrayList为例,当我们把一个Apple类型的ArrayList赋值给一个Fruit类型的引用的时候,会发现编译器是不允许我们这么做。
这就是泛型的一个设计原则。在泛型里面,Apple类型的ArrayList 和 Fruit类型的ArrayList,本质上是没有任何关系的独立的两个类型。
只有像下面那种写法,把泛型改成这种支持协变的形式,编译器才能够通过编译。所以泛型本身其实默认是具有不变性的。这样做是就是为了类型安全。
假设说泛型列表也是协变的,就像我们下面这段代码一样。假设这段代码是能够通过编译的,那么就会发生我们能够把Orange放入到一个ArrayList<Apple>
中的这种情况发生。这种情况会导致类型安全问题发生。
Java
//假设
ArrayList<Fruit> fruits = new ArrayList<Apple>();
fruits.add (new Orange());
4、破坏数组的类型安全
Java
List<String>[] strList2 = new ArrayList[10];
//破坏类型安全
Object[] objects = strList2;//String[]是Object[]的子类
objects[1] = "";
Object数组是所有数组的父类型,我们可以把任意的数组上转成 Object[]。我们就可以把任意类型的值存入到这个数组中了。这段代码从语法的角度上来说是没有任何问题的。编译期不会报错,直到运行期才会抛出一个 java.lang.ArrayStoreException
,表示我们往数组里放入了一个错误的类型。对于泛型列表来说,由于泛型默认是不变的,所以在编译期就能杜绝这样的问题发生。
这个问题的起因是因为Java早期设计数组,后来又引入了泛型。这两套东西在设计理念上是不兼容的。在没有泛型的时代,Java中不管是数组还是集合,其实都不是类型安全的。所以数组当初被设计成协变是没有问题的。但是到了泛型的时代,协变就变成了数组的一个安全的隐患。所以即使数组能够完全支持泛型,但是由于它的协变性在编译期也无法完全的去保证它的类型安全。这也是java无法去创建泛型数组的一个最重要的一个原因。
总结
Java的数组为什么不支持泛型?首先这个说法是不准确的。
泛型能在编译期一定程度的去保证数组的类型安全。Java中的所谓的不支持,只是不支持去创建泛型类型的数组。我们知道其实java中的泛型,其实是基于类型擦除机制来实现的。所以其实即使我们创建的泛型集合,也不是真正的泛型集合。第二点是由于数组的一个协变性,它的设计理念就与泛型相悖,所以泛型数组的安全性是不完全的。