java-多态参数与泛型

在使用泛型的集合在参数传递过程中使用多态可能会出现问题,与数组传递过程比较如下:

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

通过反射将不符合范型要求的数据插入集合,所以可以看出,范型只在变异期间起作用,进入运行期间是不起作用的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值