列表优先于数组
1、协变与不可变类型
1、数组是协变类型,指继承关系的
- 协变的意思就是Number是Integer的父类,那么
Number[] data = new Integer[]
;是成立的。
2、泛型是不可变类型(也就是final类型),没有继承关系。无限制通配符类型是一个特例。
- 就是说无法
List<Number> list = new List<Integer>()
//这段代码在运行时出错
Object[] o = new Long[1];
o[0] = "I don't fit it";
//这段代码在编译时出错
List<Object> o = new ArrayList<Long>();
o.add("I don't fit it";
这并不意味着泛型是有缺陷的,而实际上可以说数组才是有缺陷的。利用数组,你会在运行时发现所犯的错误,而使用列表,则可以在编译的时候发现错误。我们当然希望在编译的时候发现错误,这个也是列表优于数组的一个方面。
2、运行时检验与编译器检验
数组:运行时检验。因为数组在编译使其创建的类是具体化的。然后在运行期间检验类型约束是否正确。所以就会产生将String类
添加到Long类型
的数组中会在运行时报java.lang.ArrayStoreException
的错误。感觉这个模式是特定针对数组的协变而创造的,因为数组在编译器中也是会受检验报错的。比如说:
public static void main(String[]args){
Integer[] data = new Integer[10];
data[0] = 3.14;//在编译器直接报错
}
泛型:编译期检验。因为泛型会在运行的过程中参数类型会被擦除,所以必须在编译期就得保证类型安全。这就不容许有协变这个性质。
3、可具体化与不可具体化
数组是可以具体化的。可以具体化意味着运行时包含的信息比编译期包含的信息多。
泛型是不可以具体化的,反之,在运行时包含的信息会更少,因为编译后会被擦除(替换成Object)。
其实以上例子已经很好的说明了可具体化与不可具体的区别。数组会在运行时才知道并检查他们的元素类型约束。相比之下,泛型则是通过擦除来实现的。因此泛型只在编译时强化他们的类型信息,并在运行时丢弃他们的元素类型信息。擦除就是使泛型可以与没有使用泛型的代码随意进行互用。
4、无法很好混用列表和数组
由于上述这些根本的区别,因此数组和列表无法很好地混合使用。
- 例如创建泛型、参数化类型或者类型参数的数组都是不合法的。如
new List[]
、new List[]
和new E[]
那为什么不能创建泛型数组呢?最主要原因就是类型安全问题。因为编译器在其他正确的程序中发生的转换就会在运行时失败,并抛出ClassCastException
异常。如下所示:
List<String>[] stringLists = new List<String>[1]; //(1)其实编译不通过,我们假设可以运行,以此说明问题
List<Integer> intList = Arrays.asList(42); //(2)
Object[] objects = stringLists; //(3)
objects[0] = intList;
String s = stringLists[0].get(0);
以上代码中,代码(1)
表示新建个泛型数组,代码(2)
新建个列表,且有一个类型为Integer的元素42,由于数组是协变的,所以代码(3)
成立,就是将objects
指向stringLists
数组,代码(4)
将代码(2)
新建的列表赋值给objects
的首元素,代码(5)
表示通过stringLists
获取其中的首元素,并赋值给String
类型。大家想想,Integer
赋值给String
会产生什么,ClassCastException
就出来了。
上述例子展示了泛型数组带来的严重问题,因此泛型数组是非法的。当使用泛型数组出错是,建议考虑使用集合类型List。
混用列表和数组往往出现警告,例如Arrays.asList
,因为禁止创建泛型数组,泛型一般不可能返回他的元素类型的数组,所以意味着在结合使用可变参数方法和泛型是会出现令人费解的警告。这是由于每当调用可变参数方法时,就会创建一个数组来存放可变参数。想要禁止警告的出现,可以添加@SuppressWarnings
,或者避免在API中混用泛型和可变参数。
5、案例分析
需求:在执行运算之前需要对列表进行锁定(备份)。
假设有个同步列表list(Collections.synchronizedList
返回的)和一个函数apply
- 函数的作用是每次取列表两个数进行对应操作并返回,
- 如果列表的值为Integer,则将两个数相加,
- 如果列表的值为String,则将两个值连接。
现在需要写个方法reduce
来将列表list
应用到方法apply
上,reduce
方法除了list
和函数apply
之外,还提供个initVal
,表示默认值,如果元素为String
,则为"",如果为Integer
,则为0。代码如下所示:
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);
}
以上代码中,由于list
是同步列表synchronizedList
,所以我们可以直接采用同步列表的内置锁。从另一方面来看,上面的代码同步块中包含了其余的代码,其实仅仅在遍历的时候需要,调用apply的时候都不需要的,导致锁范围太大。优化后:
static Object reduce(List list, Function f, Object initVal) {
Object[] snapShot = list.toArray();
Object result = initVal;
for (Object o : snapShot) {
result = f.apply(result, o);
}
return result;
}
考虑到列表元素可能是Integer或者String等类型,且reduce方法尽量不要使用原型类型。如果改为了泛型:
static <E> E reduce(List<E> list, Function<E> f, E initVal) {
E[] snapShot = (E[])list.toArray();
E result = initVal;
for (E o : snapShot) {
result = f.apply(result, o);
}
return result;
}
interface Function<T> {
T apply(T arg1, T arg2);
}
此时,代码经过检验过后,会给出警告warning: [unchecked] unchecked cast found :Object[], required: E[] E[] snapshot = (E[]) list.toArray();
。这个时候由于程序是没有问题的,只能在E[] snapShot = (E[])list.toArray()
上注解@SupressWarning
。
其实如果至始至终只按照列表的操作,可以按如下所示:
static <E> E reduce(List<E> list, Function<E> f, E initVal) {
List<E> snapList;
synchronized(list) {
snapList = new ArrayList<E>(list);
}
E result = initVal;
for (E o : snapList) {
result = f.apply(result, o);
}
return result;
}
interface Function<T> {
T apply(T arg1, T arg2);
}
综上所述,数组和泛型拥有不同的类型规则。数组是协变并具象的,泛型是不变和擦除性质的。导致的结果就是数组在运行时提供安全性检查但是编译时并不能,而列表正相反。
通常来说,数组和泛型并不能很好的混合使用,如果你发现你已经混合使用了,且在代码检查过程中报出了警告,则最好采用列表代替数组。