12.5 泛型与数组

数组是Java语言中的基本数据结构,有其自身的特殊性。数组对象是由Java虚拟机根据元素类型创建出来的。数组不同于集合类对象的一个重要特征是数组是协变的(covariant)。如果一个数组类型的元素类型是另外一个数组类型的元素类型的子类型,那么这个数组类型同时也是对应的数组类型的子类型。因此,String[]类型是Object[]的子类型。这种协变关系对于集合类对象来说是不存在的。比如,List<String>并不是List<Object>的子类型。其中的原因是数组的元素类型信息在运行时仍然是被保留的,而泛型类型的类型信息因类型擦除机制而被去掉。在运行时,虚拟机可以利用数组的元素类型信息来判断对数组的操作是否合法。如果尝试向数组中添加类型不兼容的对象,那么在运行时会产生java.lang.ArrayStoreException异常。在进行类型检查时,使用的是元素的运行时类型。

1.数组声明时的可用类型

在代码清单12-14中创建了一个Integer类型的数组,但是用Object[]类型来引用。由于Integer[]是Object[]的子类型,因此把Integer[]类型的对象赋值给Object[]类型的变量是合法的。当尝试向数组中添加String类型的元素时,虽然编译时不会出现错误,但运行时会抛出ArrayStoreException异常。因为array变量所指向的实际是Integer类型的数组,所以虚拟机会根据运行时类型来进行判断。在使用泛型集合类对象时并不会出现这种问题。如果把代码改写成使用泛型集合类型的形式,如类似“List<Object>list=new ArrayList<Integer>”的方式,会发现这条语句无法通过编译。使用泛型可以避免出现类似的潜在问题。

代码清单12-14 使用数组时可能产生的运行时类型安全问题


public void storeInArray(){

Object[]array=new Integer[10];

array[0]="Hello";//抛出异常

}


由于数组的这种特殊性,除了只包含无界通配符的参数化类型和原始类型之外,其他泛型类型的实例化形式都不允许用来创建数组。也就是说,只有可具体化类型才可以用来创建数组。如果不对泛型的实例化形式做出限制,就可能造成操作数组元素时的动态类型检查失效,造成错误类型的对象被添加到数组中。在代码清单12-15的方法中,如果创建ArrayList<String>类型的数组的操作是合法的,那么尝试向该数组中添加ArrayList<Integer>类型的对象的操作不但在编译时是合法的,在运行时也不会抛出ArrayStoreException异常。原因是经过类型擦除之后,ArrayList<String>和ArrayList<Integer>的实际类型都是ArrayList,虚拟机认为这是合法的数组保存操作。但是,数组的使用者在使用数组的元素时,会遇到ClassCastException异常。为了避免出现这种问题,编译器不允许创建ArrayList<String>类型的数组,因此代码清单12-15中方法的第一行代码会出现编译错误。

代码清单12-15 创建参数化类型数组的错误示例


public void storeInGenericArray(){

Object[]array=new ArrayList<String>[10];//编译错误

array[0]=new ArrayList<Integer>();

}


唯一允许用来创建数组的是泛型类型的只包含无界通配符的实例化形式,例如,“new List<?>[]”是合法的。这是因为无界通配符表示的是所有类型的集合,不管数组中存放的元素的具体类型是什么,使用无界通配符进行引用总是合法的。不过由于无界通配符在作为对象引用时的使用限制,创建这种形式的数组的意义并不大。需要注意的是,虽然不允许创建数组,但是元素为参数化类型的数组引用是合法的,例如,“List<String>[]list=null;”是一个合法的语句。允许使用这种引用方式的原因是一个非泛型类型可以继承自某个参数化类型,而用这个非泛型类型创建数组是合法的。代码清单12-16给出了一个示例,非泛型类型StringArrayList继承自参数化类型ArrayList<String>,在createArray方法中使用StringArrayList类型来创建数组,同时用ArrayList<String>[]类型来引用这个数组。

代码清单12-16 继承自参数化类型的非泛型类型


public class StringArrayList extends ArrayList<String>{

public void createArray(){

ArrayList<String>[]array=new StringArrayList[10];

}

}


2.调用参数长度可变方法时的隐式数组的使用

另外一个与泛型和数组相关的内容是调用参数长度可变的方法。在调用参数长度可变的方法时,实际参数是通过数组来传递的,比如方法声明“void method(String……values)”实际上等价于“void method(String[]values)”。在实际调用方法时,编译器根据实际参数的个数创建一个数组对象,并把实际参数保存到数组中,再把数组传递给方法。数组的创建工作由编译器自动完成。当长度可变参数的类型是可具体化类型时,这种数组创建方式并没有问题。如果参数的类型是不可具体化类型,则可能会出现类型安全问题,这也是不允许创建元素类型为不可具体化类型的数组的原因。但是,编译器并没有禁止使用不可具体化类型作为长度可变参数的类型,只是给出相关的警告信息。忽视这些警告有可能产生运行时的异常。代码清单12-17中的varargsMethod方法的参数类型是不可具体化的List<String>。在方法实现中,可以使用Object[]类型引用作为中间变量把List<String>类型转换成List<Integer>类型,并添加Integer类型的对象到列表中。

代码清单12-17 长度可变参数的类型为不可具体化类型时存在的类型安全问题


public class Varargs{

public void varargsMethod(List<String>……values){

Object[]array=values;

List<Integer>list=(List<Integer>)array[0];

list.add(1);

}

public void useVarargsMethod(){

List<String>list=new ArrayList<>();

list.add("Hello");

varargsMethod(list);

String str=list.get(1);//抛出ClassCastException异常

}

}


当长度可变参数的类型是不可具体化类型时,如果在方法的实现中只读取参数中的内容,而不修改参数中的对象,那么不会出现类型安全问题。如果可以确定一个方法不会产生类型安全问题,那么可以使用Java SE 7中的“@SafeVarargs”注解来进行声明。编译器对添加了“@SafeVarargs”注解的方法的调用不会给出警告信息。