Java的协变、逆变与不可变
package javase; import java.util.ArrayList;
import java.util.List; class Animal{ }
class Cat extends Animal{ } public class CatIsAnimal {
public static void main(String[] args){
Animal am = new Cat();
ArrayList<Cat> aam = new ArrayList<Cat>();
//! ArrayList<Animal> aam = new ArrayList<Cat>();
Cat[] cats = new Cat[10];
Animal[] animals = new Cat[10]; ///////////////////////////////////////////
//////////////////////////////////////////
Number num = new Integer(1);
Number[] nums = new Integer[10];
//! ArrayList<Number> anum = new ArrayList<Integer>(); //error
//! List<Number> lnum = new ArrayList<Integer>(); //error
List<? extends Number> list = new ArrayList<Number>();
List<? extends Number> list2 = new ArrayList<Integer>();
List<? extends Integer> list3 = new ArrayList<Integer>(); ////////////////////////////////////////////
////////////////////////////////////////////
List<? extends Number> list4 = new ArrayList<Number>();
//! list4.add(new Integer(1));
//! list4.add(new Float(1.0f));
List<? super Number> list5 = new ArrayList<Object>();
//! List<? super Number> list6 = new ArrayList<Integer>();
list5.add(new Integer(4));
list5.add(new Float(1.2f));
}
} class Super{
Number method(Number n){
return null;
}
}
class Sub extends Super{
@Override
Integer method(Number n){
return null;
}
}
网上有位大大对这个主题讲解的比较透彻,我在下面粘贴:
看下面一段代码
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 Integer和Float?为了解决这些问题,我们需要了解Java中的逆变和协变以及泛型中通配符用法。
1. 逆变与协变
在介绍逆变与协变之前,先引入Liskov替换原则(Liskov Substitution Principle, LSP)。
Liskov替换原则
LSP由Barbara Liskov于1987年提出,其定义如下:
所有引用基类(父类)的地方必须能透明地使用其子类的对象。
LSP包含以下四层含义:
- 子类完全拥有父类的方法,且具体子类必须实现父类的抽象方法。
- 子类中可以增加自己的方法。
- 当子类覆盖或实现父类的方法时,方法的形参要比父类方法的更为宽松。
- 当子类覆盖或实现父类的方法时,方法的返回值要比父类更严格。
前面的两层含义比较好理解,后面的两层含义会在下文中详细解释。根据LSP,我们在实例化对象的时候,可以用其子类进行实例化,比如:
Number num = new Integer(1);
定义 逆变与协变用来描述类型转换(type transformation)后的继承关系,其定义:如果AA、BB表示类型,f(⋅)f(⋅)表示类型转换,≤≤表示继承关系(比如,A≤BA≤B表示AA是由BB派生出来的子类); f(⋅)f(⋅)是逆变(contravariant)的,当A≤BA≤B时有f(B)≤f(A)f(B)≤f(A)成立;
f(⋅)f(⋅)是协变(covariant)的,当A≤BA≤B时有f(A)≤f(B)成立f(A)≤f(B)成立;
f(⋅)f(⋅)是不变(invariant)的,当A≤BA≤B时上述两个式子均不成立,即f(A)f(A)与f(B)f(B)相互之间没有继承关系。
类型转换
接下来,我们看看Java中的常见类型转换的协变性、逆变性或不变性。
泛型
令f(A)=ArrayList<A>,那么f(⋅)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 Integer和Float会发生编译错误?首先,我们看看add的实现:
public interface List<E> extends Collection<E> {
boolean add(E e);
}
在调用add方法时,泛型E自动变成了<? extends Number>,其表示list所持有的类型为在Number与Number派生子类中的某一类型,其中包含Integer类型却又不特指为Integer类型(Integer像个备胎一样!!!),故addInteger时发生编译错误。为了能调用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都不用)。
参考资料:
http://www.cnblogs.com/en-heng/p/5041124.html
Java的协变、逆变与不可变的更多相关文章
- Java中的逆变与协变
看下面一段代码 Number num = new Integer(1); ArrayList<Number> list = new ArrayList<Integer>(); ...
- Java中的逆变与协变(转)
看下面一段代码 Number num = new Integer(1); ArrayList<Number> list = new ArrayList<Integer>(); ...
- Java中的逆变与协变 专题
结论先行: PECS总结: 要从泛型类取数据时,用extends: 协变 要往泛型类写数据时,用super: 逆变 既要取又要写,就不用通配符(即extends与super都不用) 不变 List&l ...
- 解读经典《C#高级编程》最全泛型协变逆变解读 页127-131.章4
前言 本篇继续讲解泛型.上一篇讲解了泛型类的定义细节.本篇继续讲解泛型接口. 泛型接口 使用泛型可定义接口,即在接口中定义的方法可以带泛型参数.然后由继承接口的类实现泛型方法.用法和继承泛型类基本没有 ...
- Java泛型的逆变
在上篇<Java泛型的协变>这篇文章中遗留以下问题——协变不能解决将子类型添加到父类型的泛型列表中.本篇将用逆变来解决这个问题. 实验准备 我们首先增加以下方法,见代码清单1所示. 代码清 ...
- C#的in/out关键字与协变逆变
C#提供了一组关键字in&out,在泛型接口和泛型委托中,若不使用关键字修饰类型参数T,则该类型参数是不可变的(即不允许协变/逆变转换),若使用in修饰类型参数T,保证"只将T用于输 ...
- C#中泛型方法与泛型接口 C#泛型接口 List<IAll> arssr = new List<IAll>(); interface IPerson<T> c# List<接口>小技巧 泛型接口协变逆变的几个问题
http://blog.csdn.net/aladdinty/article/details/3486532 using System; using System.Collections.Generi ...
- java协变逆变,PECS
public static void main(String[] args) { // Object <- Fruit <- Apple <- RedApple System.out ...
- Java中的逆变与协变 很直接不饶弯的讲出来了
```java 协变 extends只能new 辈分比自己低的家伙 List<? extends Number> list001 = new ArrayList<Integer> ...
随机推荐
- linux和windows下的命令
1.rz命令,可以传输文件 2.kill -9 杀死进程 3.windows命令: netstat -ano | findstr "3031" taskkill /f /t /i ...
- Visual Studio Error
Visual Studio Error 注意:文中所有“系统”用词,均指Windows Console操作系统IO Debug Error 错误类型 #0表示调用约定错误 可以考虑在指针前面加上_st ...
- Javascript:日期排班功能实现
背景: 近期,公司的产品经常会遇到日期排班类似的功能: 需求的排班日期长短不一:有些是两周,有些是四周:要求选中的时候有一个active的状态区分,另外要提供钩子获取选中日期的形如:[2018-04 ...
- php.ini中safe_mode开启之后对于PHP系统函数的影响
safe_mode是提供一个基本安全的共享环境. 在一个多用户共享的phpweb服务器上,当这台服务器开启了safe_mode模式,有以下函数将会受到影响. 首先,以下尝试访问文件系统的函数将会被限制 ...
- Windows 操作系统如何使程序开机自启
Windows 操作系统如何开机自启 一.前言: 作为一只运维开发,很多时候需要将自己的小工具做开机自启.在 Linux 的世界里,如果你希望一个程序可以开机自启,那么可以在/etc/rc.d/rc. ...
- IntelliJ IDEA 如何将一个 filename.java 文件直接运行
IntelliJ IDEA 如何将一个 filename.java 文件直接运行 前言: 出于考证需要,手头有许多 *.java 文件需要进行运行.然后出于对 JetBrains 全家桶的喜爱,选 ...
- P5108 仰望半月的夜空
题目链接 题意分析 给你一个字符串 让你求\(1-n\)长度下的字符串的中字典序最小并且最靠左的字符串的开头位置 我们考虑先建出\(SA\) 然后考虑对于一个字符串后缀排序之后 baba 后缀排序之后 ...
- VMware安装黑威联通教程+文件 亲身测试成功 老骥伏枥黑威联通母盘QNAP1G-BOOT 1G
1.首先致敬老骥伏枥大神的帖子. [老骥伏枥-原创]制作黑威联通启动盘:进阶篇 2.其次感谢这位作者的安装教程 作者:f541883216 [老骥伏枥-原创]用我的黑威联通启动盘在WMware搭建系统 ...
- BZOJ - 3263 三维偏序
题意:定义元素为有序组(a,b,c),若存在x组(a_i,b_i,c_i)分别小于等于(a,b,c),则该元素的等级为x,求[0,n-1]等级的个数 cdq分治练手题,对a简单排序并去重,对b进行分治 ...
- python爬虫之urllib库(三)
python爬虫之urllib库(三) urllib库 访问网页都是通过HTTP协议进行的,而HTTP协议是一种无状态的协议,即记不住来者何人.举个栗子,天猫上买东西,需要先登录天猫账号进入主页,再去 ...