Java泛型拓展

基础知识之协变与逆变

// public final class Integer extends Number  
Number num = new Integer(1);    
List<Number> list = new ArrayList<>();  
list.add(new Integer(3));  
ArrayList<Number> list = new ArrayList<Integer>(); //type mismatch 这两个根本就不是一个类型 
  
List<? extends Number> list = new ArrayList<Number>();  
list.add(new Integer(1)); //error  

为什么Number的对象可以由Integer实例化,而ArrayList<Number>的对象却不能由ArrayList<Integer>实例化?List中的<? extends Number>声明其元素是NumberNumber的派生类,为什么不能add Integer?为了解决这些问题,需要了解Java中的逆变和协变以及泛型中通配符用法。

​ Java中String类型是继承自Object的,姑且记做String ≦ Object,表示String是Object的子类型,String的对象可以赋给Object的对象。而Object的数组类型Object[],理解成是由Object构造出来的一种新的类型,可以认为是一种构造类型,记f(Object),那么可以这么来描述协变和逆变:

当A ≦ B时,如果有f(A) ≦ f(B),那么f叫做协变;

当A ≦ B时,如果有f(B) ≦ f(A),那么f叫做逆变;

如果上面两种关系都不成立则叫做不可变。

JAVA中泛型是不变的,可有时需要实现逆变与协变,怎么办呢?这时就需要通配符?

协变

<? extends Number>实现了Java中的协变

List<? extends Number> list = new ArrayList<>();  ·

<? extends Number>表示通配符?的上界为Number,即“? extends Number”可以代表Number或其子类,但代表不了Number的父类(如Object),因为通配符的上界是Number

于是有? extends Number ≦ Number,则List<? extends Number> ≦ List< Number >。那么就有:

List<? extends Number> list001 = new ArrayList<Integer>();  
List<? extends Number> list002 = new ArrayList<Float>();  

但是这里不能向list001、list002添加除null以外的任意对象。可以这样理解一下,List<Integer>可以添加Interger及其子类,List<Float>可以添加Float及其子类,List<Integer>、List<Float>都是List<? extends Animal>的子类型,如果能将Float的子类添加到List<? extends Animal>中,就说明Float的子类也是可以添加到List<Integer>中的,显然是不可行。故java为了保护其类型一致,禁止向List<? extends Number>添加任意对象,不过却可以添加null

逆变

<? super>实现了泛型的逆变,比如:

List<? super Number> list = new ArrayList<>();  

? super Number 则表示通配符?的下界为Number。为了保护类型的一致性,因为? super Number可以是Object或其他Number的父类,因无法确定其类型,也就不能往List<? super Number >添加Number的任意父类对象。但是可以向List<? super Number>添加Number及其子类。

List<? super Number> list001 = new ArrayList<Number>();  
List<? super Number> list002 = new ArrayList<Object>();  
list001.add(new Integer(3));  
list002.add(new Integer(3)); 

PECS

现在问题来了:究竟什么时候用extends什么时候用super呢?《Effective Java》给出了答案:

PECS: producer-extends, consumer-super.

PECS指“Producer Extends,Consumer Super”。换句话说,如果参数化类型表示一个生产者,就使用<? extends T>;如果它表示一个消费者,就使用<? super T>

比如一个Stack API

public class Stack<E>&#123;  
    public Stack();  
    public void push(E e):  
    public E pop();  
    public boolean isEmpty();  
&#125;  

要实现pushAll(Iterable<E> src)方法,将src的元素逐一入栈:

public void pushAll(Iterable<E> src)&#123;  
    for(E e : src)  
        push(e)  
&#125; 

​ 假设有一个实例化Stack<Number>的对象stacksrcIterable<Integer>Iterable<Float>;在调用pushAll方法时会发生type mismatch错误,因为Java中泛型是不可变的,Iterable<Integer> Iterable<Float>都不是Iterable<Number>的子类型。因此,应改为

// Wildcard type for parameter that serves as an E producer  
public void pushAll(Iterable<? extends E> src)&#123;
  for(E e : src)  
        push(e)
&#125; 

要实现popAll(Collection<E> dst)方法,将Stack中的元素依次取出adddst中,如果不用通配符实现:

// popAll method without wildcard type - deficient!  
public void popAll(Collection<E> dst) &#123;  
    while (!isEmpty())  
        dst.add(pop());     
&#125;  

同样地,假设有一个实例化Stack<Number>的对象stackdstCollection<Object>;调用popAll方法是会发生type mismatch错误,因为Collection<Object>不是Collection<Number>的子类型。因而,应改为:

public void popAll(Collection<? super E> dst)&#123;
  while(!isEmpty())&#123;
    dst.add(pop);
  &#125;
&#125;

在上述例子中,在调用pushAll方法时生产了E 实例(produces E instances),在调用popAll方法时dst消费了E 实例(consumes E instances)Naftalin与Wadler将PECS称为Get and Put Principle

java.util.Collections的copy方法(JDK1.7)完美地诠释了PECS:

public static <T> void copy(List<? super T> dest, List<? extends T> src) &#123;  
    int srcSize = src.size();  
    if (srcSize > dest.size())  
        throw new IndexOutOfBoundsException("Source does not fit in dest");  
  
    if (srcSize < COPY_THRESHOLD ||  
        (src instanceof RandomAccess && dest instanceof RandomAccess)) &#123;  
        for (int i=0; i<srcSize; i++)  
            dest.set(i, src.get(i));  
    &#125; else &#123;  
        ListIterator<? super T> di=dest.listIterator();  
        ListIterator<? extends T> si=src.listIterator();  
        for (int i=0; i<srcSize; i++) &#123;  
            di.next();  
            di.set(si.next());  
        &#125;  
    &#125;  
&#125;  

通配符的副作用

//以盘子为例
class plate<T>&#123;
  private T item;
  public Plate(T t)&#123;
    item = t;
  &#125;
  public void set(T t)&#123;
    item = t;
  &#125;
  public void get()&#123;
    return item;
  &#125;
&#125;

class Fruit&#123;&#125;
class Apple extends Apple&#123;&#125;
//上界<? enteds T>只能取,不能存。
//code block
Plate<? extends Fruit> p = new plate<Apple>(new Apple());

//不能存储任何元素;
p.set(new Fruit());
p.set(new Apple());


//读取出来的东西只能放在Fruit或者是它的基类里。
Fruit f = p.get();
Object f1 = p.get();
Apple af = p.get() // ERROR;

//其实锅都在编译器中:

原因是编译器只知道容器内是Fruit或者它的派生类,但具体是什么类型不知道。可能是Fruit?可能是Apple?也可能是Banana,RedApple,GreenApple?编译器在看到后面用Plate赋值以后,盘子里没有被标上有“苹果”。而是标上一个占位符:CAP#1,来表示捕获一个Fruit或Fruit的子类,具体是什么类不知道,代号CAP#1。然后无论是想往里插入Apple或者Meat或者Fruit编译器都不知道能不能和这个CAP#1匹配,所以就都不允许。

所以通配符<?>和类型参数的区别就在于,对编译器来说所有的T都代表同一种类型。比如下面这个泛型方法里,三个T都指代同一个类型,要么都是String,要么都是Integer。

public <T> List<T> fill(T... t);

但通配符<?>没有这种约束,Plate<?>单纯的就表示:盘子里放了一个东西,是什么我不知道。

所以题主问题里的错误就在这里,Plate<? extends Fruit>里什么都放不进去。

下界<? super T>不影响往里存,但往外取只能放在Object对象里

使用下界<? super Fruit>会使从盘子里取东西的get( )方法部分失效,只能存放到Object对象里。set( )方法正常.

p=new Plate(new Fruit());

//存入元素正常
p.set(new Fruit());
p.set(new Apple());

//读取出来的东西只![KjDLw](../../../Desktop/KjDLw.png)能存放在Object类里。
Apple newFruit3=p.get();    //Error
Fruit newFruit1=p.get();    //Error
Object newFruit2=p.get();

因为下界规定了元素的最小粒度的下限,实际上是放松了容器元素的类型控制。既然元素是Fruit的基类,那往里存粒度比Fruit小的都可以。但往外读取元素就费劲了,只有所有类的基类Object对象才能装下。但这样的话,元素的类型信息就全部丢失。

总结 PECS原则:

最后看一下什么是PECS(Producer Extends Consumer Super)原则,已经很好理解了:

  • 频繁往外读取内容的,适合用上界Extends。
  • 经常往里插入的,适合用下界Super。

The principles behind this in Computer Science is named after

  • Covariance - ? extends MyClass,
  • Contravariance - ? super MyClass and
  • Invariance/non-Variance - MyClass

The picture below should explain the concept.

KjDLw

​ (图片来源 https://stackoverflow.com/users/2707792/andrey-tyukin)

TODO :好好理解一下PECS 原则?到底什么时候是生产,什么时候是消费。生产了什么与消费了什么?

参考内容:

[1] http://www.cnblogs.com/drizzlewithwind/p/6100164.html;

Comments

2017-06-27

⬆︎TOP