数组与泛型相比,有两个重要的不同点,首先,数组是协变的(covariant)。这个词听起来有点吓人,其实只是表示如果Sub为Super的子类型,那么数组类型Sub[]就是Super[]的子类型。相反,泛型则是不可变的(invariant):对于任意两个不同类型Type1和Type2,List<Type1>既不是List<Type2>的子类型,也不是List<Type2>的超类型。你可能认为,这意味着泛型是有缺陷的,但实际上,可以说数组才是有缺陷的。
下面代码片段是合法的:
Object[] objectArray = new Long[1]; objectArray[0] = "I don't fit in";//Throws ArrayStoreExecption
但是下面的代码则是不合法的:
List<Object> ol = new ArrayList<Long>();//Incompatible types 不兼容类型 ol.add("I don't fit in");
这其中无论是那种方法,都不能将String类型放进Long容器中,但是利用数组,你会在运行时抛出所犯的错误,利用List列表,则是在编译时就能看到发生的错误,我们当然希望在编译时发现错误了。
数组与泛型之间的第二大区别在于数组是具体化的。因此数组会在运行时才知道并检查他们的元素类型的约束,。如上所述,如果企图将String保存到Long数组中,就会得到ArrayStoreExecption异常。相比之下,泛型则是通过擦除来实现的。因此泛型只在编译时强化他们的类型信息,并在运行时丢弃,他们的元素类型信息。擦除就是使泛型可以与没有使用泛型的代码随意进行交互。
由于上述这些根本的区别,因此数组和泛型不能很好的混合使用。例如,创建泛型,参数化类型或者类型参数的数组是非法的。这些数组创建表达式没有一个是合法的:new List<E>、new List<String>[]和new E[]。这些在编译的时都会导致一个generic array creation(泛型创建数组)错误。
为什么创建泛型数组是非法的呢?
因为他不是类型安全的。要是他合法,编译器在其他正确的程序中发生的转换就会在运行时失败,并出现一个ClassCastExecption异常。这样就违背了泛型系统提供的基本保证。
为了更具体的对此证明进行说明,考虑一下代码片段:
List<String>[] stringLists = new ArrayList<String>[1];//1 List<Integer> integers = Arrays.asList(42);//2 Object[] objects = stringLists;//3 objects[0] = integers;//4 String s = stringLists[0].get(0);//5
我们假设第一行是合法的,他创建了一个泛型数组,第二行创建并初始化了一个包含单个元素的List<Integer>。第三行将List<String>数组保存到一个Object数组变量中,这是合法的,因为数组是协变的。第四行将List<Integer>保存到Object数组里唯一的元素中,这是可以的,因为泛型是通过擦除实现的:
List<Integer>实例的运行时类型是List,List<String>[]实例的运行时类型则是List[],因此这种安排不会产生ArrayStoreExecption异常。但现在我们有麻烦了。我们将一个List<Integer>实例保存到了原本声明只包含List<String>实例的数组中,在第五行,我们从这个数组唯一的列表中获取了唯一的元素。编译器自动的将获取到的元素转换成String,但是是一个Integer,因此,我们在运行时会得到一个ClassStoreExecption异常,为了防止这种情况出现,就在创建泛型数组的时候(第一行)就产生了编译时的错误。
从技术角度来说,像E,List<E>,List<String>这样的类型应称为不可具体化的类型。直观点说,不可具体化的类型是指其运行的表示法包含的信息比他编译时表示法包含的信息更少的类型,唯一可具体化的参数类型是无限制的通配符类型,如:List<?>和Map<?,?>。虽然不常用,但是创建无限制通配符的数组是合法的,
禁止创建数组可能有点讨厌,例如:这表明泛型一般不能返回他的元素类型数组。这也意味着在结合使用可变参数方法和泛型时会出现令人费解的警告。这是由于每当调用可变参数方法时,就会创建一个数组来存放参数,如果这个数组的元素类型不是可具体化的,就会创建一条警告,关于这些警告,除了把他们禁止,并且避免在API中混合使用泛型与可变参数之外,别无他法。
当你得到泛型数组传创建错误时,最好的解决办法通常是优先使用集合类型List<E>,而不是数组E[],这样可能损失一些性能或者简洁性,但是换来的确实更高的类型安全性和互用性。
例如,假设有一个同步列表和一个函数,现在假设要编写一个方法reduce,并使用函数apply来处理这个列表。假设列表元素类型为整数,并且函数是用来做两个整数的求和运算,reduce方法就会返回列表中所有值的总和,如果函数是用来做两个整数求积的运算,该方法就会返回列表值积。如果列表中包含字符串,并且函数链接两个字符串,该函数就会返回一个字符串,他按顺序包含了列表中的所有字符串,除了列表和函数之外,reduce方法还采用初始值进行减法运算,列表为空时会返回这个初始值,以下是没有泛型时的代码:
static Object reduce(List list,Function f,Object initVal){ synchronized (list) { Object result = initVal; for (Object object : list) { result = f.apply(result, object); } return result; } } interface Function{ Object apply(Object args1,Object args2);
}
使用synchronized它告诉你不要从同步区域中调用“外来的方法”。因此,在持有锁的时候修改reduce方法来复制列表中的内容,也可以让你在备份上执行减法。Java1.5发行版本之前,要这么做一般利用List的toArray方法
static Object reduce(List list,Function f,Object initVal){ Object[] snapshot = list.toArray(); Object result = initVal; for (E e : snapshot) { result = f.apply(result, e); } return result; }
如果试图通过泛型来完成这一点,就会遇到我们之前讨论过的那种麻烦。一下是Function接口泛型化版本:
interface Function<T>{ T apply(T args1,T args2); }
下面是一种天真的尝试,试图将泛型应用到修改过的reduce方法,这是一个泛型方法,如果你不理解这条声明,也不必担心,对于这个条目,应该吧注意力集中在方法体上:
static <E> E reduce(List<E> list,Function<E> f,E initVal){
E[] snapshot = list.toArray();
E result = initVal;
for (E e : snapshot) {
result = f.apply(result, e);
}
return result;
}
如果试着编译这个方法,就会得到下面这个错误消息:
Multiple markers at this line
- Line breakpoint:Test2 [line: 25] - reduce(List<E>,
Function<E>, E)
- Type mismatch: cannot convert from Object[] to E[]
E[] snapshot = list.toArray();
你可能会说,这没什么大不了的,我会将Object数组转换成一个E数组:
E[] snapshot =(E[]) list.toArray();
他会消除了那条编译的错误消息,但是一条警告信息又会出现:
Multiple markers at this line
- Type safety: Unchecked cast from Object[] to E[]
- Line breakpoint:Test2 [line: 25] - reduce(List<E>,
Function<E>, E)
编译器会告诉你,他无法在运行时检查转换的安全性,因为他在运行时还不知道E是什么类型,记住,元素类型信息会在运行时从泛型中擦除。这段程序可以运行吗?结果表明,他可以运行,但是不安全。通过微小的修改,就可以让他没有包含显式转换的行上抛出ClassCastExecption异常,snapshpt的编译时类型是E[],他可以为String[] Integer[]或者任何其他类型的数组,运行时类型为Object[],这很危险,不可具体化的类型的数组转换只能在特殊情况下使用。
那么该做些什么修改呢?下面reduce方法编译时就不会再有任何警告信息了:
static <E> E reduce(List<E> list,Function<E> f,E initVal){ List<E> sanpshot; synchronized (list) { sanpshot = new ArrayList<E>(list); } E result = initVal; for (E e : sanpshot) { result = f.apply(result, e); } return result; }
这个版本的代码可以确定在运行时不会得到ClassCastExecption异常,
总而言之,数组和泛型有这不同的的类型规则。数组是协变且可以具体化的;泛型是不可变的且可以被擦除的。因此,数组提供了运行时的类型安全,但是没有编译时的类型安全,反之,对于泛型也一样,一般来说,数组和泛型不能很好的混合使用,如果你发现自己将他们混合使用,并且得到了编译时错误或者警告,你的第一反应就是应该用List列表代理数组。