数组于泛型相比较,有两个重要的不同点。首先,数组是协变的(covariant)。这个词听起来有点吓人,其实是表示如果Sub为Super的子类型,那么数字sub[]类型,就是super[]类型的子类型。相反泛型是不可变的(invariant):对于任意两个不同的类型type1和type2,list<type1>和既不是list<type2>的子类型,也不是他的超类型,你可能认为泛型是有有缺陷的,但是实际上数组才是有缺陷的 ,举个例子
//编译通过,但是会抛出异常
Object[] array = new Long[2];
array[0] = " dd";
//编译不通过
ArrayList<Long> num = new ArrayList<Long>();
num.add("dd");
上面两种方法无论哪个都无法将String放进long容器,但使用数组你在运行时候才能发现错误,而使用列表在编译的时候就知道。
数组于泛型之间的第二大区别在于,数组是具体化的(reified)因此数组会在运行时才知道并检查他们的元素类型约束。如上所示将String放到long中会得到一个ArraystoreException异常,相比之下,泛型则是通过擦除来实现的,因此泛型只在编译的时候强化类型信息,在运行的时候擦除元素类型信息
由于上述这些根本的区别,因此数组和泛型不能很好的混合使用。例如,创建泛型,参数化类型或者类型参数的数组是非法的
new list<String>[]
new list<E>[]
new E[]
这些都会在编译的时候报错:generic array creation(泛型数组创建错误)
那么为什么创建泛型数组是非法的?
因为它不是类型安全的,要是他合法,编译器在其他正确的程序中发生的转换就会在运行时候失败,并抛出一个ClassCastExeception异常,这就违背了泛型系统提供的基本特征。 举个栗子
List<String>[] stringLists=new List<String>[1];//创建一个泛型数组,假设合法
List<Integer> intList= Arrays.asList(42);//创建并初始化一个包含单个元素的List<Integer>
Object[] objects=stringLists;//将List<String>数组保存到一个Object数组变量中,这是合法的,因为数组和协变的。
objects[0]=intList;//将List<Integer>保存到Object数组里唯一的元素中,这是可以的,因为泛型是通过擦除实现的。
String s=stringLists[0].get(0);//我们从这个数组里唯一的列表中获取唯一的元素,编译器会自动地获取到元素转换成String,但它是一个Integer,因此,我们在运行时得到一个ClassCastException。
//为了防止这种情况(创建泛型数组),第一行就产生了一个编译时错误。
当你得到泛型数组创建错误时候,最好的办法就是优先使用集合类型List<E>,而不是数组类型E[],举个复杂栗子
假设有一个(collections.synchronizedList返回的那种)同步列表和一个函数,假设要编写一个方法reduce,并使用apply函数来处理这个列表,列表元素类型为整数,并且函数是用来做两个整数的求和运算,reduce就会返回列表中所有元素的总和,如果函数是返回两个数的乘积,那么reduce就返回整个列表的乘积,代码如下
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);
}
第67条中告诉我们:不要从同步区域中调用外来方法,因此在持有锁的时候修改reduce方法来复制列表中的内容,要这么做一般用List的toArray方法(它在内部锁定列表)
static Object reduce(List list,Function f,Object initVal){
Object [] snaphot = list.toArray();//lock list
Object result = initVal;
for(Object o:snaphot){
result = f.apply(result,o);
}
return result;
}
如果视试图用泛型来完成这一点,就会遇到之前讨论的麻烦,以下是Function的泛型版
interface Function<T>{
T apply(T arg1,T arg2);
}
那么我们来天真的修改reduce方法
static <E> E reduce(List<E> list,Function<E> f,E initVal){
E [] snaphot = list.toArray();//error required E[] found Object[]
E result = initVal;
for(E o:snaphot){
result = f.apply(result,o);
}
return result;
}
由于上述编译错误,你可能会说,没什么大不了的,将类型强转就行
E [] snaphot = (E[]) list.toArray();
这样错误是消除了 但是却出现了一条警告
image.png
编译器告诉我们,它无法在运行时候检查转换的安全性,因为它在运行时候还不知道E是什么,这段程序虽然是可以运行的但是,通过微小的改动,就可以让它在没有包含显示转换的行上抛出ClassCastExeception
那么应该怎么办??? 就是用列表代替数组 代码如下:
static <E> E reduce(List<E> list, Function<E> f, E initVal) {
List<E> snaphot;
synchronized (list) {
snaphot = new ArrayList<>(list);
}
E result = initVal;
for (E o : snaphot) {
result = f.apply(result, o);
}
return result;
}
这个版本的代码虽然冗长,但是可以确定在运行时候不会得到任何的转换异常。
数组和泛型有着非常不同的类型规则。数组是协变且可以具体化的;泛型是不可变的且可以被擦除的。因此,数组提供了运行时的类型安全,但是没有编译时的类型安全,反之,对于泛型也一样。一般来说,数组和泛型不能很好地混合使用。如果你发现自己将它们混合起来使用,并且得到了编译时错误或者警告,你的第一反应就应该是用列表代替数组。
网友评论