在使用泛型的集合在参数传递过程中使用多态可能会出现问题,与数组传递过程比较如下:
public void go(){
Animal[] animals={new Dog(),new Cat()};
Dog[] dogs = new { new Dog(),new Dog(),new Dog()};
operateMethod(animals);
operateMethod(dogs);
}
operateMethod方法中使用Animal数组作为参数
public void operateMethod(Animal[] animals){
for(Animal a:animals){
a.eat(); //这里只可调用animal中定义的方法
}
}
使用数组不会参数传递过程中使用多态出现错误。
但将数组array换成arrayList集合就出现编译错误
public void go(){
List<Animal> animals=new ArrayList<Animal>(); //这个与形参匹配编译可以通过
animals.add(new Dog());
List<Dog> dogs = new ArrayList<Dog>(); //编译不通过
dogs.add(new Dog());
dogs.add(new Dog());
operateMethod(animals);
operateMethod(dogs);
}
operateMethod方法中使用ArrayList<Animal>作为参数
public void operateMethod(ArrayList<Animal> animals){
for(Animal a:animals){
a.eat();
}
}
原因由于数组类型实在运行期间检查,但集合的类型检查只在编译期间(如果dogs编译通过,向集合中加入一个cat也会通过)
但数组由于是运行期间检查,在foreach中如果执行animals[0] = new Cat();就会发现错误。但集合animals.add(new Cat())却不会发现错误
因此如果能保证集合参数传递编译通过,就需要保证不会有向dogs集合中加入cat这样可能破坏引用参数所指集合的行为,java中使用万用字符实现这一功能。
使用万用字符接收Animal子类型参数的方法:public void operateMethod(ArrayList<? extends Animal> animals)
或者另一种形式:public <T extends Animal> void operateMethod(ArrayList<T> animals)
两种形式区别在于当使用T时第二种方便:public <T extends Animal> void operateMethod(ArrayList<T> one,ArrayList<T> two)
注意:上面两种方式都与public void operateMethod(ArrayList<Animal> animals)这种方式声明的方法不同,这种方式只能使用ArrayList<Animal>作为实参,ArrayList<Dog>, ArrayList<Cat>都不行。
参数中使用万用字符时,可以调用list中的任何元素的方法,但不能加入元素,也即是可以操作集合元素,但不能新增集合元素,这样才能保证执行期间的安全性。
泛型的类型擦出
上面的泛型不支持多态参数的深层次原因是泛型的类型擦除,泛型的类型擦除见下面例子,当我们定义带有泛型的类时,编译完成后将会将用到泛型的地方替换成object。
public class Node<T> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData() { return data; }
// ...
}
编译器做完类型检查后,实际在运行期间代码将转化成:
public class Node {
private Object data;
private Node next;
public Node(Object data, Node next) {
this.data = data;
this.next = next;
}
public Object getData() { return data; }
// ...
}
但是当我们在泛型上加上限定通配符后,编译后则会替换成接口类型/父类类型而不是Object,如下:
public class Node<T extends Comparable<T>> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData() { return data; }
// ...
}
编译完成后:
public class Node {
private Comparable data;
private Node next;
public Node(Comparable data, Node next) {
this.data = data;
this.next = next;
}
public Comparable getData() { return data; }
// ...
}
这也就解释了为什么泛型的类型检查只是在编译期,因为在运行期间泛型都被类型擦除,运行期间往Dog的集合中加入Cat是不会报错的,数组在运行期间进行检查,往String的数组中加入int还是会报错的。因此java中数组中也不支持泛型,反向思考,如果数组支持泛型,过了编译器这一关,编译完成后会发生类型擦除,JVM实际上根本就不知道new ArrayList<String>()
和new ArrayList<Integer>()
的区别,这样运行期间发生的错误也就不能检测。如下所示:
Object[] strings = new String[2];
strings[0] = "hi"; // OK
strings[1] = 100; // An ArrayStoreException is thrown.
假如数组支持泛型的话:
Object[] stringLists = new List<String>[]; // compiler error, but pretend it's allowed
stringLists[0] = new ArrayList<String>(); // OK
// An ArrayStoreException should be thrown, but the runtime can't detect it.
stringLists[1] = new ArrayList<Integer>();
由于类型擦除,运行时就不能检查到错误。
反射绕过编译期检查的范型
由于范型只在编译期进行检查,可通过反射在运行期间绕过范型检查,使范型生效。
public class Test {
public static void main(String[] args) {
List<Integer> list1 = new ArrayList<>();
List list2 = new ArrayList();
Class c1 = list1.getClass();
Class c2 = list2.getClass();
System.out.println(c1 == c2);
try {
Method method = c1.getMethod("add", Object.class);
method.invoke(list1, "123");
method.invoke(list1, "456");
System.out.println(list1.size());
} catch (Exception e) {
e.printStackTrace();
}
}
}
输出为:
true, 1
通过反射将不符合范型要求的数据插入集合,所以可以看出,范型只在变异期间起作用,进入运行期间是不起作用的。