术语:
协变的(convariant):表示如果Sub为super的子类型,那么数组类型Sub[]就是super[]的子类型。
不可变的(invariant):对于任意两个不同的类型Type1和Type2,List<Type1>既不是List<Type2>的子类型,也不是List<Type2>的超类型。
不可具体化的(non-reifiable):指其运行时表示法包含的信息比它编译时表示法包含的信息更少的类型。
数组与泛型之间有几个明显的区别,第一、数组是协变的而泛型是不可变的。这二者不同会带来什么问题呢?看看下面的例子:
// Fails at runtime!
Object[] objectArray = new Long[1];
objectArray[0] = "I don't fit in"; // Throws ArrayStoreException
// Won't compile
List<Object> ol = new ArrayList<Long>(); // Incompatible types
ol.add("I don't fit in");
从上例的第一段代码可以看出,数组是在运行时才发现异常,而泛型则在编译时就发现异常,这就是数组是协变的造成的,因为Long是Object的子类型,所以Long[]是Object[]的子类型,这样一来,在第二句将一个String插入到一个此时类型为Object的数组里在编译期是合法的(多态的特征是在运行时展现的),然而,当运行的时候就会发现,噢!原来我在把一个String的对象放到Long类型的容器中,这怎么可以呢?而对于泛型,由于在运行时,泛型类型会被擦除(正解决了C++中模板代码膨胀的问题),所以在编译其就会检查类型匹配问题。由于泛型是不可变的,所以ArrayList<Long>并不是List<Object>的子类,在编译时编译器会给出错误信息。这两个小小的例子还告诉我们,使用泛型可以将某些数组带来的运行时的错误提早到编译时被发现,这当然是所期望的。还有一点,泛型是不可以用基本类型来做类型参数的,相应的,可以使用基本类型的自动装箱类型来实现。
第二、数组是可具体化的,而泛型不是。例如:创建泛型、参数化类型或者是类型参数的数组都是非法的。如:new List<E>[]、new List<String>[]和new E[]都是非法的。这些在编译的时候会产生一个generic array creating错误。要说明这种行为为什么会被禁止掉,先看一下如下的例子:
List<String>[] stringLists = new List<String>[1];
List<Integer> intList = Arrays.asList(42);
Object[] objects = stringLists;
objects[0] = intList;
String s = stringLists[0].get(0);
假设第一行是合法的,由于数组是协变的第3行将List<Integer>保存到Object数组里唯一的元素中这是合法的。而第四行将List<Integer>保存到Object数组里的元素中也是合法的,这是因为泛型是通过擦除来实现的。List<Integer>在运行时类型只是List。假设上述条件都满足,但是最后一行当取出一对应的元素的时候,在运行时我们发现,怎么是个Integer?目标不是String吗?为了防止这种问题出现,第1行就产生了一个编译时错误。
像E、List<E>、List<String>这样的类型应称作不可具体化的类型。唯一可具体化的参数化类型是无限制的通配符类型,如List<?>和Map<?,?>。虽然不常用,但是创建无限制通配类型的数组是合法的。
当得到泛型数组创建错误时,最好的解决办法通常是优先使用集合类型List<E>,而不是数组类型E[]。这样可能会损失一些性能或者简洁性,但是换回的却是更高的类型安全和互用性。考虑如下示例:
// Reduction without generics, and with concurrency flaw!
static Object reduce(List list, Function f, Object initVal) {
synchronized(list) {
Object result = initVal;
for (Object o : list)
result = f.apply(result, o);
return result;
}
}
interface Function {
Object apply(Object arg1, Object arg2);
}
我们不应该在同步区域中调用外来的方法,因此,要在持有锁的时候修改reduce方法来复制列表中的内容,在这备份上来执行操作。下面是一个使用JDK1.5之前的toArray的做法(它在内部锁定列表):
// Reduction without generics or concurrency flaw!
static Object reduce(List list, Function f, Object initVal) {
Object[] snapshot = list.toArray();//Locks list internally
Object result = initVal;
for (Object e: snapshot)
result = f.apply(result, e);
}
好了,再让我们来试一泛型版本:
interface Function<T> {
T apply(T arg1, T arg2);
}
// Naive generic version of reduction - won't compile!
static <E> E reduce(List<E> list, Function<E> f, E initVal) {
E[] snapshot = list.toArray(); // Locks list
E result = initVal;
for (E e: snapshot)
result = f.apply(result, e);
return result;
}
由于toArray方法返回的是一个Object数组,见如下摘自List接口的声名
Object[] toArray();
现在将一个Object[]赋给E[]的变量显然出现了问题(假设可行)。解决这个问题好像加一个强制类弄转换就可以办到了。但是加了强转以后又会得到一条警告。编译器无法在运行时检查转换的安全性,因为它在运行时不知道E是什么类型,当然这只是一条警告,但是要注意到,编译时类开E,可以是String、Integer等等,但是运行时类型却成了Object(因为运行时泛型的擦除动作),这是很危险的。为了解决这个问题,我们可以用列表来试一试:
// List-based generic reduction
static <E> E reduce(List<E> list, Function<E> f, E initVal) {
List<E> snapshot;
synchronized(list) {
snapshot = new ArrayList<E>(list);
}
E result = initVal;
for (E e: snapshot)
result = f.apply(result, e);
return result;
}
注意,这段代码使用了synchronized代码块(看了源码,toArray与ArrayList构造函数都是使用同一个复制函数,不知道是不是因为JDK版本的原因)。这个版本的代码比数组版的代码稍微长了一些,但是可以确定在运行时不会得到转换异常。
总之,数组和泛型有着非常不同的类型规则。数组是协变且可具体化的,泛型是不可变的且是可被擦除的。因此,数组提供了运行时的类型安全,但是没有编译时的类型安全,反之,泛弄也一样,一般来说,数组和泛型不能很好的混合使用,如果发现自己将它们混合起来使用,并且得到了编译时错误或警告,那么就要考虑用列表代替数组。