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> ...
随机推荐
- 串口实现FIFO接受数据(V2)
在上一次的基础上添加了不同需求缓冲区大小可变的更改. /* * 串口的FIFO简单读取实现 * 功能,实现串口的FIFO实现 * 使用方法: * 更新时间:2017.9.26 * 版本:v2.0.0 ...
- Ubuntu64 apache2+lvs+Keepalived
安装 apache2, vim, keepalived 和 ipvsadm 打开 vim /etc/keepalived/keepalived.conf 点击 i, 进入编辑状态, 输入: ! Con ...
- SpringMVC 的使用映射路径展示文件服务器上的图片
需求: 前端上传图片,返回路径给前端,前端通过路径来访问图片的资源. 解决方案: <mvc:resources mapping="/${mapping_path}/**" l ...
- CSS3水平翻转样式和background-size兼容问题
一.水平翻转和垂直翻转:第一种:随着现代浏览器对CSS3的支持愈发完善,对于实现各个浏览器兼容的元素的水平翻转或是垂直翻转效果也就成为了可能.相关的CSS代码如下: /*水平翻转*/ .flipx { ...
- [ActionScript 3.0] 透视投影
下面的示例演示如何使用透视投影来创建 3D 空间.该示例演示如何通过projectionCenter属性来修改消失点和更改空间的透视投影.进行这种修改后,将强制重新计算focalLength和fiel ...
- SQLMAP 基础操作
SQLMAP 基础操作 sudo git clone https://github.com/sqlmapproject/sqlmap GET 请求: -u POST 请求: Option: --dat ...
- CentOS 中文支持
其中通过 yum 方式安装的最简单快速: yum groupinstall "Chinese Support" 装完中文语言包之后,它不会自己变成中文,得修改文件/etc/sysc ...
- django2使用xadmin打造适合国人的后台管理系统(1)
python火了之后,学习python的人也越来越多了,python做web开发的话,flask.django是比较火的框架了,django是一个比较大的框架,也是一个快速开发利器.但是,django ...
- ORACLE中的KEEP()使用方法
转载至:http://blog.csdn.net/aqszhuaihuai/article/details/6434160 ====================================== ...
- python学习,day3:函数式编程
调用函数来实现文件的修改(abc.txt),并增加上时间,调用的是time模块, 需要注意的是,每个函数一定要用‘’‘ ‘’’ 标注下函数说明 # coding=utf-8 # Author: RyA ...