Java-泛型中的extends和super关键

通配符


在本文的前面的部分里已经说过了泛型类型的子类型的不相关性。但有些时候,我们希望能够像使用普通类型那样使用泛型类型:


向上造型一个泛型对象的引用

向下造型一个泛型对象的引用

向上造型一个泛型对象的引用


例如,假设我们有很多箱子,每个箱子里都装有不同的水果,我们需要找到一种方法能够通用的处理任何一箱水果。更通俗的说法,A是B的子类型,我们需要找到一种方法能够将C《A》类型的实例赋给一个C《B》类型的声明。


为了完成这种操作,我们需要使用带有通配符的扩展声明,就像下面的例子里那样:



List《Apple》 apples = new ArrayList《Apple》();

List《? extends Fruit》 fruits = apples;

“? extends”是泛型类型的子类型相关性成为现实:Apple是Fruit的子类型,List《Apple》 是 List《? extends Fruit》 的子类型。


向下造型一个泛型对象的引用


现在我来介绍另外一种通配符:? super。如果类型B是类型A的超类型(父类型),那么C《B》 是 C《? super A》 的子类型:


List《Fruit》 fruits = new ArrayList《Fruit》();

List《? super Apple》 = fruits;

为什么使用通配符标记能行得通?


原理现在已经很明白:我们如何利用这种新的语法结构?



让我们重新看看这第二部分使用的一个例子,其中谈到了Java数组的子类型相关性:



Apple[] apples = new Apple[1];

Fruit[] fruits = apples;

fruits[0] = new Strawberry();

就像我们看到的,当你往一个声明为Fruit数组的Apple对象数组里加入Strawberry对象后,代码可以编译,但在运行时抛出异常。


现在我们可以使用通配符把相关的代码转换成泛型:因为Apple是Fruit的一个子类,我们使用? extends 通配符,这样就能将一个List《Apple》对象的定义赋到一个List《? extends Fruit》的声明上:



List《Apple》 apples = new ArrayList《Apple》();

List《? extends Fruit》 fruits = apples;

fruits.add(new Strawberry());

这次,代码就编译不过去了!Java编译器会阻止你往一个Fruit list里加入strawberry。在编译时我们就能检测到错误,在运行时就不需要进行检查来确保往列表里加入不兼容的类型了。即使你往list里加入Fruit对象也不行:


1

fruits.add(new Fruit());

你没有办法做到这些。事实上你不能够往一个使用了? extends的数据结构里写入任何的值。


原因非常的简单,你可以这样想:这个? extends T 通配符告诉编译器我们在处理一个类型T的子类型,但我们不知道这个子类型究竟是什么。因为没法确定,为了保证类型安全,我们就不允许往里面加入任何这种类型的数据。另一方面,因为我们知道,不论它是什么类型,它总是类型T的子类型,当我们在读取数据时,能确保得到的数据是一个T类型的实例:


1

Fruit get = fruits.get(0);

? super


使用 ? super 通配符一般是什么情况?让我们先看看这个:


List《Fruit》 fruits = new ArrayList《Fruit》();

List《? super Apple》 = fruits;

我们看到fruits指向的是一个装有Apple的某种超类(supertype)的List。同样的,我们不知道究竟是什么超类,但我们知道Apple和任何Apple的子类都跟它的类型兼容。既然这个未知的类型即是Apple,也是GreenApple的超类,我们就可以写入:



fruits.add(new Apple());

fruits.add(new GreenApple());

如果我们想往里面加入Apple的超类,编译器就会警告你:



fruits.add(new Fruit());

fruits.add(new Object());

因为我们不知道它是怎样的超类,所有这样的实例就不允许加入。


从这种形式的类型里获取数据又是怎么样的呢?结果表明,你只能取出Object实例:因为我们不知道超类究竟是什么,编译器唯一能保证的只是它是个Object,因为Object是任何Java类型的超类。


存取原则和PECS法则


总结 ? extends 和 the ? super 通配符的特征,我们可以得出以下结论:


如果你想从一个数据类型里获取数据,使用 ? extends 通配符

如果你想把对象写入一个数据结构里,使用 ? super 通配符

如果你既想存,又想取,那就别用通配符。

这就是Maurice Naftalin在他的《Java Generics and Collections》这本书中所说的存取原则,以及Joshua Bloch在他的《Effective Java》这本书中所说的PECS法则。


Bloch提醒说,这PECS是指”Producer Extends, Consumer Super”,这个更容易记忆和运用。


http://www.importnew.com/14985.html


 


什么是PECS? 


PECS指“Producer Extends,Consumer Super”。换句话说,如果参数化类型表示一个生产者,就使用《? extends T》;如果它表示一个消费者,就使用《? super T》,可能你还不明白,不过没关系,接着往下看好了。


下面是一个简单的Stack的API接口:



public class  Stack《E》{

    public Stack();

    public void push(E e):

    public E pop();

    public boolean isEmpty();

}

假设想增加一个方法,按顺序将一系列元素全部放入Stack中,你可能想到的实现方式如下:


public void pushAll(Iterable《E》 src){

    for(E e : src)

        push(e)

}

假设有个Stack《Number》,想要灵活的处理Integer,Long等Number的子类型的集合


Stack《Number》 numberStack = new Stack《Number》();

Iterable《Integer》 integers = ....;

numberStack.pushAll(integers);

此时代码编译无法通过,因为对于类型Number和Integer来说,虽然后者是Number的子类,但是对于任意Number集合(如List《Number》)不是Integer集合(如List《Integer》)的超类,因为泛型是不可变的。


幸好java提供了一种叫有限通配符的参数化类型,pushAll参数替换为“E的某个子类型的Iterable接口”:


public void pushAll(Iterable《? extends E》 src){

    for (E e: src)

        push(e);

}

这样就可以正确编译了,这里的《? extends E》就是所谓的 producer-extends。这里的Iterable就是生产者,要使用《? extends E》。因为Iterable《? extends E》可以容纳任何E的子类。在执行操作时,可迭代对象的每个元素都可以当作是E来操作。


与之对应的是:假设有一个方法popAll()方法,从Stack集合中弹出每个元素,添加到指定集合中去。


public void popAll(Collection《E》 dst){

       if(!isEmpty()){

                dst.add(pop());

        }

}

假设有一个Stack《Number》和Collection《Object》对象:


Stack《Number》 numberStack = new Stack《Number》();

Collection《Object》 objects = ...;

numberStack.popAll(objects);

同样上面这段代码也无法通过,解决的办法就是使用Collection《? super E》。这里的objects是消费者,因为是添加元素到objects集合中去。使用Collection《? super E》后,无论objects是什么类型的集合,满足一点的是他是E的超类,所以不管这个参数化类型具体是什么类型都能将E装进objects集合中去。


总结:


如果你是想遍历collection,并对每一项元素操作时,此时这个集合是生产者(生产元素),应该使用 Collection《? extends Thing》.

如果你是想添加元素到collection中去,那么此时集合是消费者(消费元素)应该使用Collection《? super Thing》

注:此文根据《Effective Java》以及Java Generics: What is PECS? 整理成文。想了解更多有关泛型相关知识,请读者阅读《Effective Java》的第五章。




泛型是在Java 1.5中被加入了,这里不讨论泛型的细节问题,这个在Thinking in Java第四版中讲的非常清楚,这里要讲的是super和extends关键字,以及在使用这两个关键字的时候为什么会不同的限制。 

   首先,我们定义两个类,A和B,并且假设B继承自A。下面的代码中,定义了几个静态泛型方法,这几个例子随便写的,并不是特别完善,我们主要考量编译失败的问题: 

public class Generic{  

//方法一  

public static 《T extends A》 void get(List《T extends A》 list)  

{  

    list.get(0);  

}    

//方法二  错误的方法

public static 《T extends A》 void set(List《T extends A》 list, A a)  

{  

    list.add(a);  

}    

//方法三  错误的方法

public static 《T super B》 void get(List《T super B》 list)  

{  

    list.get(0);  

}  

  

//方法四  

public static 《T super B》 void set(List《T super B》 list, B b)  

{  

    list.add(b);  

}  

}  


  编译之后,我们会发现,方法二和方法三没有办法通过编译。按照Thinking in Java上的说法,super表示下界,而extends表示上界,方法二之所以没有办法通过,是因为被放到List里面去的可能是A,也可能是任何A的子类,所以编译器没有办法确保类型安全。而方法三之所以编译失败,则是因为编译器不知道get出来的是B还是B的其他的什么子类,因为set方法四允许在list放入B,也允许在list中放入B的子类,也就没有办法保证类型安全。 

  上面的这段解释听起来可能有点奇怪,都是因为编译器无法判断要获取或者设置的是A和B本身还是A和B的其他的子类才导致的失败。那么Java为什么不干脆用一个关键字来搞定呢? 

  如果从下面的角度来解释,就能把这个为什么编译会出错的问题解释的更加的直白和清除,也让人更容易理解,先看下面的代码,还是A和B两个类,B继承自A: 

B继承自A

public class Generic2{  

   public static void main(String[] args){  

      List《? extends A》 list1 = new ArrayList《A》();  

      List《? extends A》 list2 = new ArrayList《B》();  

      List《? super B》 list3 = new ArrayList《B》();  

      List《? super B》 list4 = new ArrayList《A》();  

   }  

}  


   从上面这段创建List的代码我们就更加容易理解super和extends关键字的含义了。首先要说明的一点是,Java强制在创建对象的时候必须给类型参数制定具体的类型,不能使用通配符,也就是说new ArrayList《? extends A》(),new ArrayList《?》()这种形式的初始化语句是不允许的。 

   从上面main函数的第一行和第二行,我们可以理解extends的含义,在创建ArrayList的时候,我们可以指定A或者B作为具体的类型,也就是,如果《? extends X》,那么在创建实例的时候,我们就可以用X或者扩展自X的类为泛型参数来作为具体的类型,也可以理解为给?号指定具体类型,这就是extends的含义。 

   同样的,第三行和第四行就说明,如果《? super X》,那么在创建实例的时候,我们可以指定X或者X的任何的超类来作为泛型参数的具体类型。 

   当我们使用List《? extends X》这种形式的时候,调用List的add方法会导致编译失败,因为我们在创建具体实例的时候,可能是使用了X也可能使用了X的子类,而这个信息编译器是没有办法知道的,同时,对于ArrayList《T》来说,只能放一种类型的对象。这就是问题的本质。而对于get方法来说,由于我们是通过X或者X的子类来创建实例的,而用超类来引用子类在Java中试合法的,所以,通过get方法能够拿到一个X类型的引用,当然这个引用可以指向X也可以指向X的任何子类。 

   而当我们使用List《? super X》这种形式的时候,调用List的get方法会失败。因为我们在创建实例的时候,可能用了X也可能是X的某一个超类,那么当调用get的时候,编译器是无法准确知晓的。而调用add方法正好相反,由于我们使用X或者X的超类来创建的实例,那么向这个List中加入X或者X的子类肯定是没有问题的,因为超类的引用是可以指向子类的。 

  最后还有一点,这两个关键字的出现都是因为Java中的泛型没有协变特性的倒置的。

posted @ 2017-06-24 18:50  Wishmeluck  阅读(175)  评论(0编辑  收藏  举报