Java中的逆变与协变

我不是女神ヾ 2021-07-24 14:48 584阅读 0赞

看下面一段代码

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

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

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

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

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

LSP包含以下四层含义:

  • 子类完全拥有父类的方法,且具体子类必须实现父类的抽象方法。
  • 子类中可以增加自己的方法。
  • 当子类覆盖或实现父类的方法时,方法的形参要比父类方法的更为宽松。
  • 当子类覆盖或实现父类的方法时,方法的返回值要比父类更严格。
    前面的两层含义比较好理解,后面的两层含义会在下文中详细解释。根据LSP,我们在实例化对象的时候,可以用其子类进行实例化,比如:

Number num = new Integer(1);
定义
逆变与协变用来描述类型转换(type transformation)后的继承关系,其定义:如果A、B表示类型,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,那么f(⋅)时逆变、协变还是不变的呢?如果是逆变,则ArrayList是ArrayList的父类型;如果是协变,则ArrayList是ArrayList的子类型;如果是不变,二者没有相互继承关系。开篇代码中用ArrayList实例化list的对象错误,则说明泛型是不变的。

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

  1. 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):

  1. static Number method(Number num) {
  2. return 1;
  3. }
  4. //加入Java开发交流君样:756584822一起吹水聊天
  5. Object result = method(new Integer(2)); //correct
  6. Number result = method(new Object()); //error
  7. Integer result = method(new Integer(2)); //error

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

  1. class Super {
  2. Number method(Number n) { ... }
  3. }
  4. class Sub extends Super {
  5. @Override
  6. Number method(Number n) { ... }
  7. }

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

  1. class Super {
  2. Number method(Number n) { ... }
  3. }
  4. class Sub extends Super {
  5. @Override
  6. Integer method(Number n) { ... }
  7. }//加入Java开发交流君样:756584822一起吹水聊天

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

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

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

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

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

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

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

PECS

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

PECS: producer-extends, consumer-supe

比如,一个简单的Stack API:

  1. public class Stack<E>{
  2. public Stack();
  3. public void push(E e):
  4. public E pop();
  5. public boolean isEmpty();
  6. }//加入Java开发交流君样:756584822一起吹水聊天

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

  1. public void pushAll(Iterable<E> src){
  2. for(E e : src)
  3. push(e)
  4. }

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

  1. // Wildcard type for parameter that serves as an E producer
  2. public void pushAll(Iterable<? extends E> src) {
  3. for (E e : src)
  4. push(e);
  5. }//加入Java开发交流君样:756584822一起吹水聊天

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

  1. // popAll method without wildcard type - deficient!
  2. public void popAll(Collection<E> dst) {
  3. while (!isEmpty())
  4. dst.add(pop());
  5. }//加入Java开发交流君样:756584822一起吹水聊天

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

  1. // Wildcard type for parameter that serves as an E consumer
  2. public void popAll(Collection<? super E> dst) {
  3. while (!isEmpty())
  4. dst.add(pop());
  5. }//加入Java开发交流君样:756584822一起吹水聊天

在上述例子中,在调用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:

  1. public static <T> void copy(List<? super T> dest, List<? extends T> src) {
  2. int srcSize = src.size();
  3. if (srcSize > dest.size())
  4. throw new IndexOutOfBoundsException("Source does not fit in dest");
  5. if (srcSize < COPY_THRESHOLD ||
  6. (src instanceof RandomAccess && dest instanceof RandomAccess)) {
  7. for (int i=0; i<srcSize; i++)
  8. dest.set(i, src.get(i));
  9. } else {
  10. ListIterator<? super T> di=dest.listIterator();
  11. ListIterator<? extends T> si=src.listIterator();
  12. for (int i=0; i<srcSize; i++) {
  13. di.next();
  14. di.set(si.next());
  15. }
  16. }
  17. }//加入Java开发交流君样:756584822一起吹水聊天

PECS总结:

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

最新2020整理收集的一些高频面试题(都整理成文档),有很多干货,包含mysql,netty,spring,线程,spring cloud、jvm、源码、算法等详细讲解,也有详细的学习规划图,面试题整理等,需要获取这些内容的朋友请加Q君样:756584822

发表评论

表情:
评论列表 (有 0 条评论,584人围观)

还没有评论,来说两句吧...

相关阅读

    相关 JAVA

    协变逆变的概念 可变性是以一种类型安全的方式,将一个对象当做另一个对象来使用。如果不能将一个类型替换成为另一个类型,那么这个类型就称之为:不变量。 协变:如果某个返回的

    相关 Typescript

    最近用TS时碰到协变和逆变的一些概念,发现有篇外国人写的文章比较容易理解的,这里记录下。 1. 协变和逆变简单理解 先简单说下协变和逆变的理解。 首先,无论协变还是逆

    相关 之疑问

    前言 关于协变和逆变已经有很多园友谈论过了,学习时也参考过园友们的文章,非常之到位!这个问题可能对您而言很简单,若有解释,请告知,在此感谢。高手绕道! 既然是标题是协变

    相关 详解

    逆变(`contravariant`)与协变(`covariant`)是`C4`新增的概念,许多书籍和博客都有讲解,我觉得都没有把它们讲清楚,搞明白了它们,可以更准确地去定义泛