看下面一段代码

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的派生类,为什么不能add IntegerFloat?为了解决这些问题,我们需要了解Java中的逆变和协变以及泛型中通配符用法。

1. 逆变与协变

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

Liskov替换原则

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

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

LSP包含以下四层含义:

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

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

Number num = new Integer(1);

定义

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

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

类型转换

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

泛型

f(A)=ArrayList<A>,那么\(f(\cdot)\)时逆变、协变还是不变的呢?如果是逆变,则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.

Java中的逆变与协变的更多相关文章

  1. Java中的逆变与协变(转)

    看下面一段代码 Number num = new Integer(1); ArrayList<Number> list = new ArrayList<Integer>(); ...

  2. Java中的逆变与协变 专题

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

  3. Java中的逆变与协变 很直接不饶弯的讲出来了

    ```java 协变 extends只能new 辈分比自己低的家伙 List<? extends Number> list001 = new ArrayList<Integer> ...

  4. OOP中的逆变和协变

    逆变和协变在存在于强类型语言中,尽管非常少提及.可是里面蕴含了面向对象的世界观.感谢和我一起讨论这个问题的人. 这里用了C#.Scala的语法作为演示样例.事实上逆变和协变的概念跟语言本身关系不大.事 ...

  5. C#中的逆变和协变

    msdn 解释如下: “协变”是指能够使用与原始指定的派生类型相比,派生程度更大的类型. “逆变”则是指能够使用派生程度更小的类型. 解释的很正确,大致就是这样,不过不够直白. 直白的理解: “协变” ...

  6. Java 逆变与协变的名词说明

    最近在研究Thinking in Java的时候,感觉逆变与协变有点绕,特意整理一下,方便后人.我参考于Java中的逆变与协变,但是该作者整理的稍微有点过于概念化,我在这里简单的说一下 我对于协变于逆 ...

  7. Java 逆变与协变

    最近一直忙于学习模电.数电,搞得头晕脑胀,难得今天晚上挤出一些时间来分析一下Java中的逆变.协变.Java早于C#引入逆变.协变,两者在与C#稍有不同,Java中的逆变.协变引入早于C#,故在形式没 ...

  8. .NET Core CSharp初级篇 1-8泛型、逆变与协变

    .NET Core CSharp初级篇 1-8 本节内容为泛型 为什么需要泛型 泛型是一个非常有趣的东西,他的出现对于减少代码复用率有了很大的帮助.比如说遇到两个模块的功能非常相似,只是一个是处理in ...

  9. Scala 深入浅出实战经典 第81讲:Scala中List的构造是的类型约束逆变、协变、下界详解

    王家林亲授<DT大数据梦工厂>大数据实战视频 Scala 深入浅出实战经典(1-97讲)完整视频.PPT.代码下载:百度云盘:http://pan.baidu.com/s/1c0noOt6 ...

随机推荐

  1. iOS 删除已经配置的类库和移除CocoaPods

    引言 我们使用CocoaPods非常高效地将一些第三方类库导入到我们的项目中,但是不由得产生一个疑问:如果发现某个类库不适用,甚至是整个CocoaPods我们都不想再在项目中持有,那么我们要怎么把这些 ...

  2. SQL Saturday活动再起

    SQL Saturday活动再起 时间:2015年05月09日(星期六) 地点:上海徐汇区港汇2座10楼(10.073) 我们相约港汇2座10楼(10.073),SQL PASS上海分会的SQLSat ...

  3. java HashMap那点事

    集合类的整体架构 比较重要的集合类图如下:   有序否 允许元素重复否 Collection 否 是 List 是 是 Set AbstractSet 否 否 HashSet TreeSet 是(用二 ...

  4. libQtCassandra 0.5.0 发布

    libQtCassandra 0.5.0 修复了 QCassandraRow::exists() 函数的问题,更新了 Thrift 库. libQtCassandra 是一个高级的 C++ 库用来访问 ...

  5. 需要知道关于struct的一些事情

    前言 重构代码的时候,会遇到长参数的方法,此时就需要使用“引入参数对象”来封装这些参数.大多数时候,这些参数都是简单类型,而且所有参数的值占用的空间也不是非常的大,此时使用对象真的好吗?对象的特性是堆 ...

  6. Docker Machine v1.11.2安装与使用

    官方文档:Docker Machine 官方文档:Docker Toolbox boot2docker安装包官网下载链接:Docker Toolbox-1.11.1b.exe 此安装包包含的“boot ...

  7. dijit样式定制(三)Button、RadioButton、CheckBox

    dijit.form.DropDownButton dijit的button中除了ComboButton使用table布局外,其他的button都是用span嵌套布局,下图中可看一下button的主要 ...

  8. Chrome扩展程序的二次开发:把它改得更适合自己使用

    我当然知道未经作者允许修改别人程序是不道德的了,但作为学习研究之用还是无可厚非,这里仅供交流. 一切都是需求驱动的 话说某天我在网上猎奇的时候无意间发现这么一款神奇的谷歌浏览器插件:Extension ...

  9. MySQL—FOREIGN KEY

    作用:保持数据一致性,完整性.实现一对一或一对多关系.(学习的过程中,老师说,实际的生产中,一般不使用物理上的外键约束的,都是使用逻辑上的外键约束) 要求: 父表与子表的存储引擎必须相等,而且只能是I ...

  10. Java集合框架知识总结

    两大体系:Collection,Map 一.Collection: List 接口 : List:里面对象全部是有序的(通过三种方法来遍历) ArrayList,LinkedList,Vertor A ...