zoukankan      html  css  js  c++  java
  • Java中的逆变与协变 专题

    结论先行:

    PECS总结:
    要从泛型类取数据时,用extends;  协变
    要往泛型类写数据时,用super;    逆变
    既要取又要写,就不用通配符(即extends与super都不用) 不变

    List<?>是一个泛型,在没有赋值之前,表示它可以接受任何类型的集合赋值,赋值之后就不能往里面添加元素了。

    List<?>称为通配符集合。它可以接受任何类型的集合引用赋值,不能添加任何元素,但可以remove或clear,并非immutable集合。
    List<?>一般作为参数来接收外部的集合,或者返回一个不知道具体元素类型的集合。

    List<T>最大的问题是只能放置一种类型,如果随意转换类型的话,就是“破窗理论”,泛型就失去了类型安全的意义。

    如果需要放置多种受泛型约束的类型呢?
    <? extends T>与<? super T>两种语法,但是两者的区别非常微妙。
    简单来说,
    <? extends T>是Get First,适用于,消费集合元素为主的场景;  put功能受限
    <? super T>是Put First,适用于,生产集合元素为主的场景; get功能受限

    <? extends T>可以赋值给任何T及T子类的集合,上界为T,取出来的类型带有泛型限制,向上强制转型为T。null可以表示任何类型,所以除null外,任何元素都不得添加进<? extends T>集合内。
    <? super T>可以赋值给任何T及T的父类集合,下界为T。在生活中,投票选举类似于<? super T>操作。选举代表时,你只能往里投票,取数据时,根本不知道是谁票,相当于泛型丢失。




    看下面一段代码

    Number num = new Integer(1);  
    ArrayList<Number> list = new ArrayList<Integer>(); //type mismatch
    
    List<? extends Number> list = new ArrayList<Number>();
    list.add(new Integer(1)); //error
    list.add(new Float(1.2f));  //error

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

    1. 逆变与协变

    在介绍逆变与协变之前,先引入Liskov替换原则(Liskov Substitution Principle, LSP)。

    Liskov替换原则

    LSP由Barbara Liskov于1987年提出,其定义如下:

    所有引用基类(父类)的地方必须能透明地使用其子类的对象。

    LSP包含以下四层含义:

    • 子类完全拥有父类的方法,且具体子类必须实现父类的抽象方法。
    • 子类中可以增加自己的方法。
    • 当子类覆盖或实现父类的方法时,方法的形参要比父类方法的更为宽松。
    • 当子类覆盖或实现父类的方法时,方法的返回值要比父类更严格。

    前面的两层含义比较好理解,后面的两层含义会在下文中详细解释。根据LSP,我们在实例化对象的时候,可以用其子类进行实例化,比如:

    Number num = new Integer(1); 

    定义

    逆变与协变用来描述类型转换(type transformation)后的继承关系,其定义:如果AB表示类型,f(⋅)表示类型转换,≤表示继承关系(比如,A ≤ B表示A是由B派生出来的子类);

    • f(⋅)是逆变(contravariant)的,当A ≤ B时有f(B)≤f(A)成立;
    • f(⋅)是协变(covariant)的,当A ≤ B时有f(A)≤f(B)
    • f(⋅)是不变(invariant)的,当A ≤ B时上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关系。

    类型转换

    接下来,我们来看看Java中常见类型转换的协变性、逆变性或不变性。

    泛型

    f(A)=ArrayList<A>,那么f(⋅)是逆变、协变还是不变的呢?如果是逆变,则ArrayList<Integer>ArrayList<Number>的父类型;如果是协变,则ArrayList<Integer>ArrayList<Number>的子类型;如果是不变,二者没有相互继承关系。开篇代码中用ArrayList<Integer>实例化list的对象错误,则说明泛型是不变的。

    数组

    f(A)=[]A,容易证明数组是协变的:

    Number[] numbers = new Integer[3]; 

    方法

    方法的形参是协变的、返回值是逆变的:


    通过与网友iamzhoug37的讨论,更新如下。

    调用方法result = method(n);根据Liskov替换原则,传入形参n的类型应为method形参的子类型,即typeof(n)≤typeof(method's parameter);result应为method返回值的基类型,即typeof(methods's return)≤typeof(result)

    static Number method(Number num) {
        return 1;
    }
    
    Object result = method(new Integer(2)); //correct
    Number result = method(new Object()); //error
    Integer result = method(new Integer(2)); //error

    在Java 1.4中,子类覆盖(override)父类方法时,形参与返回值的类型必须与父类保持一致:

    class Super {
        Number method(Number n) { ... }
    }
    
    class Sub extends Super {
        @Override 
        Number method(Number n) { ... }
    }

    从Java 1.5开始,子类覆盖父类方法时允许协变返回更为具体的类型:

    class Super {
        Number method(Number n) { ... }
    }
    
    class Sub extends Super {
        @Override 
        Integer method(Number n) { ... }
    }

    2. 泛型中的通配符

    实现泛型的协变与逆变

    Java中泛型是不变的,可有时需要实现逆变与协变,怎么办呢?这时,通配符?派上了用场:

    • <? extends>实现了泛型的协变,比如:
    List<? extends Number> list = new ArrayList<Integer>();
    • <? super>实现了泛型的逆变,比如:
      List<? super Number> list = new ArrayList<Object>();

    extends与super

    为什么(开篇代码中)List<? extends Number> list在add IntegerFloat会发生编译错误?首先,我们看看add的实现:

    public interface List<E> extends Collection<E> {
        boolean add(E e);
    }

    在调用add方法时,泛型E自动变成了<? extends Number>,其表示list所持有的类型为在Number与Number派生子类中的某一类型,其中包含Integer类型却又不特指为Integer类型(Integer像个备胎一样!!!),故add Integer时发生编译错误。

    为了能调用add方法,可以用super关键字实现:

    List<? super Number> list = new ArrayList<Object>();
    list.add(new Integer(1));
    list.add(new Float(1.2f));

    <? super Number>表示list所持有的类型为在Number与Number的基类中的某一类型,其中Integer与Float必定为这某一类型的子类;所以add方法能被正确调用。从上面的例子可以看出,extends确定了泛型的上界,而super确定了泛型的下界。

    PECS

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

    PECS: producer-extends, consumer-super.

    比如,一个简单的Stack API:

    public class  Stack<E>{
        public Stack();
        public void push(E e):
        public E pop();
        public boolean isEmpty();
    }

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

    public void pushAll(Iterable<E> src){
        for(E e : src)
            push(e)
    }

    假设有一个实例化Stack<Number>的对象stack,src有Iterable<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) {
        for (E e : src)
            push(e);
    }

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

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

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

    // Wildcard type for parameter that serves as an E consumer
    public void popAll(Collection<? super E> dst) {
        while (!isEmpty())
            dst.add(pop());
    }

    在上述例子中,在调用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) {
        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)) {
            for (int i=0; i<srcSize; i++)
                dest.set(i, src.get(i));
        } else {
            ListIterator<? super T> di=dest.listIterator();
            ListIterator<? extends T> si=src.listIterator();
            for (int i=0; i<srcSize; i++) {
                di.next();
                di.set(si.next());
            }
        }
    }

    PECS总结:

    • 要从泛型类取数据时,用extends;
    • 要往泛型类写数据时,用super;
    • 既要取又要写,就不用通配符(即extends与super都不用)。

    3. 参考资料

    [1] meriton, Covariance, Invariance and Contravariance explained in plain English?.
    [2] Bert F, Difference between <? super T> and <? extends T> in Java.
    [3] Joshua Bloch, Effective Java.

    http://www.cnblogs.com/en-heng/p/5041124.html

    《Thinking in Java》中说很多原因促成了泛型的出现,最引人注目的一个原因就是为了创造容器类。这个要怎么来理解呢?我的理解是,可以抛开这个为了创造容器类这个,而是回到泛型的目的是限定某种类型上来。

    所以我们现在能小结一下Object和T很重要的两点区别就是:

    1. Object范围非常广,而T从一开始就会限定这个类型(包括它可以限定类型为Object)。
    2. Object由于它是所有类的父类,所以会强制类型转换,而T从一开始在编码时(注意是在写代码时)就限定了某种具体类型,所以它不用强制类型转换。(之所以要强调在写代码时是因为泛型在虚拟机中会被JVM擦除掉它的具体类型信息,这点可参考泛型,在这里不做引申)。

    https://www.cnblogs.com/yulinfeng/p/6056038.html

  • 相关阅读:
    Treap 树堆 容易实现的平衡树
    (转)Maven实战(二)构建简单Maven项目
    (转)Maven实战(一)安装与配置
    根据请求头跳转判断Android&iOS
    (转)苹果消息推送服务器 php 证书生成
    (转)How to renew your Apple Push Notification Push SSL Certificate
    (转)How to build an Apple Push Notification provider server (tutorial)
    (转)pem, cer, p12 and the pains of iOS Push Notifications encryption
    (转)Apple Push Notification Services in iOS 6 Tutorial: Part 2/2
    (转)Apple Push Notification Services in iOS 6 Tutorial: Part 1/2
  • 原文地址:https://www.cnblogs.com/softidea/p/5122304.html
Copyright © 2011-2022 走看看