Java泛型之通配符

这篇博文主要记录学习Java编程思想的一些心得和体会。在这篇文中可能会引用一些优秀博文的内容,我会在文章末尾注明引用博文的地址。

通配符

首先我们就给出一个程序作为入口:

class Fruit{}
class Apple extends Fruit{}
class Jonathan extends Apple{}
class Orange extends Fruit{}
public class Demo01{
    public static void main(String[] args) {
        Fruit[] fruit = new Apple[10];
        fruit[0] = new Apple();
        fruit[1] = new Jonathan();
        System.out.println(fruit[0]+","+fruit[1]);
        //fruit[0] = new Fruit();
        fruit[1] = new Orange();
        System.out.println(fruit[0]+","+fruit[1]);
    }
} //output:
generics.Apple@4926097b,generics.Jonathan@762efe5d
Exception in thread "main" java.lang.ArrayStoreException: generics.Fruit
    at generics.Demo01.main(Demo01.java:13)

运行结果已经给出来了:当然,程序在运行的时候报错了。错误的原因就是:在Apple数组中放入了不是Apple类型的数据。那么,又有一个问题出现了。我们在向数组中放入错误的数据类型,编译器为什么在编译的时候没有报错呢?比如下面的程序就会在编译期的时候报错提示:

class Fruit{}
class Apple extends Fruit{}
class Jonathan extends Apple{}
class Orange extends Fruit{}
public class Demo01{
    public static void main(String[] args) {
        Apple[] apple = new Apple[10];
        apple[0] = new Apple();
        apple[1] = new Jonathan();
        System.out.println(apple[0]+","+apple[1]);
        //error:Type mismatch: cannot convert from Fruit to Apple
        //apple[0] = new Fruit();
        //apple[1] = new Orange();
        System.out.println(apple[0]+","+apple[1]);
    }
} 

这段程序就会在编译时期对放入到数组中的数据类型不符合,而报错。

解答:其实在第一段程序中,我们是用Fruit[]引用去接收一个Apple[],Fruit数组引用类型来接收Apple数组,当我们通过Fruit数组引用来对Apple数组添加Fruit类型的数据和Orange类型的数据,编译器是通过的。因为,它是Fruit[]的引用,那又有什么理由不让它添加自己的类型和其子类型的数据呢!
当然,我们也可以这样理解,不知是否理解正确(如有不正确,还望提示):我们知道,Java除了private、static、final等修饰的方法不是动态绑定的,其他方法都为动态绑定。那么,我们可以把Fruit[] fruit = new Apple[];这样理解:首先,Apple是Fruit的子类,这里定义的为数组(我们可以把它当成一种特殊的方法),其次,将Apple数组返回给Fruit数组的引用(相当于进行了向上转型)。这样我们就可以相识的认为,在向数组中加入数据的时候,编译器在编译阶段也不知道加入的数据是放在哪个具体类型的数组中,只有当程序运行的时候,动态的判断数组的类型。

当然,我们从反编译后的代码可以更加明显的知道结果:我们从头到尾只是创建了一个Apple[]对象。

 Code:
       0: bipush        10
       2: anewarray     #16                 // class generics/Apple
       5: astore_1
       6: aload_1
       7: iconst_0
       8: new           #16                 // class generics/Apple
      11: dup
      12: invokespecial #18                 // Method generics/Apple."":()V
      15: aastore
      16: aload_1
      17: iconst_1
      18: new           #19                 // class generics/Jonathan
      21: dup
      22: invokespecial #21                 // Method generics/Jonathan."":()V
      25: aastore
      26: getstatic     #22                 // Field java/lang/System.out:Ljava/io/PrintStream;
      29: new           #28                 // class java/lang/StringBuilder
      32: dup
      33: invokespecial #30                 // Method java/lang/StringBuilder."":()V
      36: aload_1
      37: iconst_0
      38: aaload
      39: invokevirtual #31                 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
      42: ldc           #35                 // String ,
      44: invokevirtual #37                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      47: aload_1
      48: iconst_1
      49: aaload
      50: invokevirtual #31                 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
      53: invokevirtual #40                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      56: invokevirtual #44                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      59: aload_1
      60: iconst_0
      61: new           #50                 // class generics/Fruit
      64: dup
      65: invokespecial #52                 // Method generics/Fruit."":()V
      68: aastore
      69: aload_1
      70: iconst_1
      71: new           #53                 // class generics/Orange
      74: dup
      75: invokespecial #55                 // Method generics/Orange."":()V
      78: aastore
      79: getstatic     #22                 // Field java/lang/System.out:Ljava/io/PrintStream;
      82: new           #28                 // class java/lang/StringBuilder
      85: dup
      86: invokespecial #30                 // Method java/lang/StringBuilder."":()V
      89: aload_1
      90: iconst_0
      91: aaload
      92: invokevirtual #31                 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
      95: ldc           #35                 // String ,
      97: invokevirtual #37                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
     100: aload_1
     101: iconst_1
     102: aaload
     103: invokevirtual #31                 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
     106: invokevirtual #40                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
     109: invokevirtual #44                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
     112: return
}

那么我们已经给上面出现的问题给出了一些解释,接下来,我们就来了解一下,如何才能让上面这种方式也能在编译时期就能通过过报错提示呢?我们其实知道,上面保存的原因就是在数组中加入了不是其对于类型的数据,那么我们只需要限定加入到数组中的数据类型就可以做到这一点啦!限定类型恰好就是泛型所擅长的事情。

于是我们就选择用泛型容器来代替数组,看还会出现什么问题:

class Fruit{}
class Apple extends Fruit{}
class Jonathan extends Apple{}
class Orange extends Fruit{}
public class Demo01{
    public static void main(String[] args) {
        //error:Type mismatch: cannot convert from ArrayList to List
        List fruit = new ArrayList();
    }
}

当用泛型容器来代替数组,在将一个Apple容器赋给Fruit容器的时候就编译不通过,那是为什么呢?

解答:首先,第一:不能把一个涉及到Apple的泛型赋给一个涉及到Fruit泛型(虽然Apple是Fruit的子类型,但是泛型在运行时期会被擦除,所以编译器并不知道泛型的类信息,就更别说判断他们是不是父子关系了)。第二:泛型没有内建的协变类型(和数组不同,数组有内建的协变类型.协变:如果一个父容器可以持有子类的容器,我们就称为发生了协变。)

解答了上面的问题过后。我们现在回到主线上,但是我们现在需要泛型子容器赋给泛型父容器,也就是说我们需要泛型父容器可以持有泛型子类的容器。那么我们该怎么做呢?
解答:其实上面我们的需求就是通配符所允许的。,此时,我们就可以改写程序如下:

class Fruit{}
class Apple extends Fruit{}
class Jonathan extends Apple{}
class Orange extends Fruit{}
public class Demo01{
    public static void main(String[] args) {
        List extends Fruit> fruit = new ArrayList();
        //error:The method add(capture#4-of ? extends Fruit) 
        //in the type List is not applicable for the arguments (Object)
        //fruit.add(new Apple());
        //fruit.add(new Orange());
        //fruit.add(new Fruit());
        //fruit.add(new Object());
    }
}

这段程序已经能够让泛型父容器持有泛型子类的容器了(也就是现在fruit容器具有协变类型啦)。但是,我们却不能向该容器中添加任何东西进行。那究竟是为什么呢?

首先我们先来了解一下:fruit类型List

public class Demo01{
    private T value;
    public Demo01() {}
    public Demo01(T val) {
        this.value = val;
    }

    public void set(T val) {
        this.value = val;
    }

    public T get() {
        return value;
    }

    public boolean equals(Object obj) {
        return value.equals(obj);
    }

    public static void main(String[] args) {
        Demo01 apple = new Demo01<>(new Apple());
        Apple a = apple.get();
        System.out.println(a);
        apple.set(a);
        System.out.println(apple.equals(a));

        Demo01 fruit = apple;
        Fruit f = fruit.get();
        System.out.println(f);
        //不能向fruit容器中填加数据,因为fruit容器中是存放的Fruit类型或其子类型中的某一具体类型
        //在调用set方法的时候,编译器也不知道你设置的数据类型时不是所需要的那种类型,所以,我们不能向里面设置值
        //fruit.set(a);
        System.out.println(fruit.equals(a));
    }
}//output:
generics.Apple@7637f22
true
generics.Apple@7637f22
true

我们一步一步来分析上面的程序:
1. 为什么value具有equals方法:因为value是泛型参数中的类型参数,当编译器编译程序的时候会把泛型参数擦除到他的第一边界处,由于我们并没有给出泛型参数T的第一边界,所以会擦除到Object处,自然equals方法就是Object中方法啦。
2. 当我们用一个持有Apple类型的容器可以持有Apple类型的数据并且能向该容器中设置新的Apple类型数据。但是,当我们通过持有Fruit的父容器去持有Apple类型的容器时,就需要定义父容器的类型为

class Fruit{}
class Orange extends Fruit{}
class Jonathan extends Apple{}
class Apple extends Fruit{}
public class Demo01{
    static List apples = new ArrayList<>();
    static List fruits = new ArrayList<>();

    static  void writeExact(List super T> list,T item) {
        list.add(item);
    }

    public static void main(String[] args) {
        writeExact(apples, new Apple());
        writeExact(fruits,new Apple());
        writeExact(fruits,new Fruit());
    }
}

这段程序,就是通过定义通配符?的下界为泛型参数类型。可以认为具有T类型为下界的容器,可以向该容器红添加T及其子类型的任意数据。因为他都同属于

class Fruit{}
class Orange extends Fruit{}
class Jonathan extends Apple{}
class Apple extends Fruit{}
public class Demo01{
    static List apples = new ArrayList<>();
    static List fruits = new ArrayList<>();

    static  void writeExact(List list,T item) {
        list.add(item);
    }

    public static void main(String[] args) {
        writeExact(apples, new Apple());
        writeExact(fruits,new Apple());
        writeExact(fruits,new Fruit());
    }
}

这个程序可以照常的运行起来,没有任何问题。那么,我们又有一个问题了,既然这两个程序都能做到相同的事,那么他们又有什么区别呢?
解答:
第一:使用下界通配符的容器和直接不使用通配符的容器都一样,可以向里面放入参数类型的数据和参数类型的派生类型数据。但是,两种有明显不同的意义:有统配符的代表容器中存放的数据都是某一具体类型的导出类型的数据,其实在编译器提示的时候可以看出来,使用通配符的容器,add方法里面其实可以存放Object导出类型的所有数据,但是我们却只能存放下界类型的数据和导出类型数据。不使用通配符,代表容器限定的类型只能为该泛型类型的数据和导出类型数据。

public class Demo01{
    public static void main(String[] args) {
        List super Apple> list = new ArrayList();
        list.add(new Apple());
        list.add(new Jonathan());
        List list2 = new ArrayList();
        list2.add( new Apple());
        list2.add(new Jonathan());
    }
}

第二:从使用超类通配符的容器中获取数据,返回值的类型只能是Object类型。而使用泛型参数的容器获取的数据类型时和泛型参数类型一致。

public class Demo01{
    public static void main(String[] args) {
        List list = new ArrayList();
        list.add(new Apple());
        Object object = list.get(0);
        System.out.println(object);
        List list2 = new ArrayList();
        list2.add( new Apple());
        Apple apple = list2.get(0);
        System.out.println(apple);
    }
}//output:
generics.Apple@7637f22
generics.Apple@4926097b

已经给出了上面的解释啦。为了更加好的扩展一下,我们可以思考一个问题:当我们使用通配符的时候,不指定通配符的上界和下界的容器既List

public class Demo01{
    static List list1;
    static List> list2;
    static List extends Object> list3;

    static void assign1(List list) {
        list1=list;
        list2=list;
        list3=list;
    }

    static void assign2(List> list) {
        list1=list;
        list2=list;
        list3=list;
    }

    static void assign3(List extends Object> list) {
        list1=list;
        list2=list;
        list3=list;
    }

    public static void main(String[] args) {
        assign1(new ArrayList());
        assign2(new ArrayList());
        assign3(new ArrayList());

        assign1(new ArrayList());
        assign2(new ArrayList());
        assign3(new ArrayList());
    }
}

通过这段程序,大家可以发现,编译器很少关心无界通配符的容器和原生容器之间的关系。就感觉

public class Demo01{
    public static void main(String[] args) {
        List list1 = new ArrayList();
        list1.add(new Object());
        list1.add(new Integer(5));
        list1.add(new String("a"));

        List list2 = new ArrayList<>();
        //error:The method add(capture#1-of ?) in the type List is not applicable for the arguments (Object)
        //list2.add(new Object());
        //error:The method add(capture#1-of ?) in the type List is not applicable for the arguments (Integer)
        //list2.add(new Integer(5));
        //error:The method add(capture#1-of ?) in the type List is not applicable for the arguments (String)
        //list2.add(new String("a"));

        ListObject> list3 = new ArrayList<>();
        //error:The method add(capture#1-of ? extends Object) in the type ListObject> is not applicable for the arguments (Object)
        //list3.add(new Object());
        //error:The method add(capture#1-of ? extends Object) in the type ListObject> is not applicable for the arguments (Integer)
        //list3.add(new Integer(5));
        //error:The method add(capture#2-of ? extends Object) in the type ListObject> is not applicable for the arguments (String)
        //list3.add(new String("a"));

        ListObject> list4 = new ArrayList<>();
        list4.add(new Object());
        list4.add(new Integer(5));
        list4.add(new String("a"));
    }
}

从这段程序我们可以看出,当使用无界通配符的时候,List

问题

上面对泛型和通配符做出了各种的解释,接下来,我们就来说一说泛型的问题。

1.任何基本数据类型不能作为泛型的参数

关于这个问题,就不做过多的解释啦。

2.实现参数化接口

也就是说一个类实现了同一个泛型接口的两种变体(也就是说泛型接口中的泛型参数不同)。其实由于泛型擦除的原因,就算两种变体,也会成为相同的接口。

首先,我们来探讨一下,一个类可以同时继承连个相同的接口吗?
答案:当然是否定的。

interface Payable{}
//error:Duplicate interface Payable for the type A
//class A implements Payable,Payable{}

但是我们如果写成下面这样,程序却没有报错:

interface Payable{}
class A implements Payable{}
class B extends A implements Payable{}

那么接下来,我们就来讨论一个类实现泛型接口的两种变体的话,那么结果如何:

interface Payable<T>{}
class Employee implements Payable<Employee>{}
//The interface Payable cannot be implemented more than once with different arguments: 
//Payable and Payable
//public class Demo01 extends Employee implements Payable{}

结果是不能进行编译的:报错的原因是:一个接口不能实现两个不同的参数

那么,我们将实现的接口都使用相同的参数呢?

interface Payable<T>{}
class Employee implements Payable<Employee>{}
public class Demo01 extends Employee implements Payable<Employee>{}

神奇的是通过了编译啦!

3.重载

用带有泛型参数是不能区分函数的重载的。原因就是因为擦除。

interface Payable{}
public class Demo01{
    //Erasure of method f(Payable) 
    //is the same as another method in type Demo01
    //error:
    public void f(Payable p) {

    }
    //error:
    public void f(Payable p2) {

    }

}

这段程序当然是不能进行编译的啦!

对大概的泛型做了一定的解释后,接下来,我们就继续进入下一部分。

自限定类型

关于自限定类型,刚开始,还是令我非常的难以理解,通过对各个类之间的关系,进行详细的分析,到后面还是比较容易理解的.

那我就直接就给出一段代码来进行分析了哟:

class SelfBounded<T extends SelfBounded<T>>{}

上面这段代码,看起来的确是感觉怪怪的。那我们就直接来分析这个程序吧:首先,我们可以知道SelfBounded是一个泛型类,有一个泛型参数T,同时也可以发现该泛型参数有一个上界是SelfBounded。然后,我们可以发现,参数类型T的上界是SelfBounded,而该上界也是一个泛型类,同样有一个泛型参数是T。也就是说,T有一个边界限定,而这个边界就是拥有T作为其参数的自身。

接下来,我们又看下面这段程序:

class SelfBounded<T extends SelfBounded<T>>{
    private T entity;

    public SelfBounded set(T obj){
        this.entity = obj;
        return this;
    }

    public T get() {
        return entity;
    }
}

class A extends SelfBounded<A>{};
class B extends SelfBounded<A>{}

class E{}
//Bound mismatch: The type E is not a 
//valid substitute for the bounded parameter > 
//of the type SelfBounded
class F extends SelfBounded<E>{}

对于这个程序,我们主要是看报错的这点:为什么E却不能作为SelfBounded的参数呢?
解答:首先SelfBounded由于泛型擦除,会将类型参数全部替换为SelfBounded类型,由于E不是SelfBounded的子类,所以,E不能作为SelfBounded的类型参数传进去。那么,为什么A却能作为参数类型传进去呢?因为,我们在定义A的时候,就申明了A是SelfBounded的子类。那么,当我们把上面的程序改一下,就会发现怎么觉得这种解释就有问题了呢?

class SelfBounded<T extends SelfBounded<T>>{
    private T entity;

    public SelfBounded set(T obj){
        this.entity = obj;
        return this;
    }

    public T get() {
        return entity;
    }
}

class A extends SelfBounded<A>{}
class B extends SelfBounded<A>{}

//Bound mismatch: The type B is not a valid substitute 
//for the bounded parameter > of the type SelfBounded
class F extends SelfBounded<B>{}

这个程序,我们用B作为参数类型,发现程序依然报错,报错的原因和上面一模一样,那这个报错的原因究竟是什么原因呢?
因为,在上面我们直接是了继承和子类的一个关系,却忽略了对SelfBounded

你可能感兴趣的:(Javase)