java 协变 逆变_JAVA中的协变与逆变
JAVA中的协变与逆变
首先说一下关于Java中协变,逆变与不变的概念
比较官方的说法是逆变与协变描述的是类型转换后的继承关系。
定义A,B两个类型,A是由B派生出来的子类(A<=B),f()表示类型转换如new List();
协变: 当A<=B时,f(A)<=f(B)成立
逆变: 当A<=B时,f(B)<=f(A)成立
不变: 当A<=B时,上面两个式子都不成立
这么说可能理解上有些费劲,我们用代码来表示一下协变和逆变
class Fruit {}
class Apple extends Fruit {}
class Jonathan extends Apple {}
class Orange extends Fruit {}
@Test
public void testArray() {
Fruit[] fruit = new Apple[10];
fruit[0] = new Apple();
fruit[1] = new Jonathan();
try {
fruit[0] = new Fruit();
} catch (Exception e) {
System.out.println(e);
}
try {
fruit[0] = new Orange();
} catch (Exception e) {
System.out.println(e);
}
}
Java中数组是协变的,可以向子类型的数组赋基类型的数组引用。
Apple是Fruit的子类型,所以Apple的对象可以赋给Fruit对象。Apple<=Fruit Fruit的数组类型是Fruit[],这个就是由Fruit对象构造出来的新的类型,即f(Fruit),同理,Apple[]就是Apple构造出来的新的类型,就是f(Apple)
所以上方代码中的Fruit[] fruit = new Apple[10]是成立的,这也是面向对象编程中经常说的
子类变量能赋给父类变量,父类变量不能赋值给子类变量。
上方代码中的try..catch中的在编译器中是不会报错的,但是在运行的时候会报错,因为在编译器中数组的符号是Fruit类型,所以可以存放Fruit和Orange类型,但是在运行的时候会发现实际类型是Apple[]类型,所以会报错
java.lang.ArrayStoreException: contravariant.TestContravariant$Fruit
java.lang.ArrayStoreException: contravariant.TestContravariant$Orange
不变
@Test
public void testList() {
List fruitList = new ArrayList();
}
这样的代码在编译器上会直接报错。和数组不同,泛型没有内建的协变类型,使用泛型的时候,类型信息在编译期会被类型擦除,所以泛型将这种错误检测移到了编译器。所以泛型是 不变的
泛型的协变
但是这样就会出现一些很别扭的情况,打个比方就是一个可以放水果的盘子里面不能放苹果。
所以为了解决这种问题,Java在泛型中引入了通配符,使得泛型具有协变和逆变的性质, 协变泛型的用法就是 extends Fruit>
@Test
public void testList() {
List extends Fruit> fruitList = new ArrayList();
// 编译错误
fruitList.add(new Apple());
// 编译错误
fruitList.add(new Jonathan());
// 编译错误
fruitList.add(new Fruit());
// 编译错误
fruitList.add(new Object());
}
当使用了泛型的通配符之后,确实可以实现将ArrayList进行向上转型了,实现了泛型的协变,但是却再也不能往容器中放任何东西了,连Apple本身都被禁止了
因为,在定义了fruitList之后,编译器只知道容器中的类型是Fruit或者它的子类,但是具体什么类型却不知道,编译器不知道能不能比配上就都不允许比配了。类比数组,在编译器的时候数组允许向数组中放Fruit和Orange等非法类型,但是运行时还是会报错,泛型是将这种检查移到了编译期,协变的过程中丢失了类型信息。
所以对于通配符,T和?的区别在于,T是一个具体的类型,但是?编译器并不知道是什么类型。不过这种用法并不影响从容器中取值。
List extends Fruit> fruitList = new ArrayList();
Fruit fruit = fruitList.get(0);
Object object = fruitList.get(0);
// 编译错误
Apple apple = fruitList.get(0);
泛型的逆变
@Test
public void testList() {
List super Apple> appleList = new ArrayList();
// 编译错误
Fruit fruit = appleList.get(0);
// 编译错误
Apple apple = appleList.get(0);
// 编译错误
Jonathan jonathan = appleList.get(0);
Object object = appleList.get(0);
appleList.add(new Apple());
appleList.add(new Jonathan());
// 编译错误
appleList.add(new Fruit());
// 编译错误
appleList.add(new Object());
}
可以看到使用super就可以实现泛型的逆变,使用super的时候指出了泛型的下界是Apple,可以接受Apple的父类型,既然是Apple的父类型,编辑器就知道了向其中添加Apple或者Apple的子类是安全的了,所以,此时可以向容器中进行存,但是取的时候编辑器只知道是Apple的父类型,具体什么类型还是不知道,所以只有取值会出现编译错误,除非是取Object类型。
泛型协变逆变的用法
当平时定义变量的时候肯定不能像上面的例子一样使用泛型的通配符,具体的泛型通配符的使用方法在Effective Jave一书的第28条中有总结:
为了获得最大限度的灵活性,要在表示生产者或者消费者的输入参数上使用通配符类型。如果每个输入参数既是生产者,又是消费者,那么通配符类型对你就没有什么好处了:因为你需要的是严格的类型比配,这是不用任何通配符而得到的。
简单来说就是PECS表示->producer-extends,consumer-super。
不要使用通配符类型作为返回类型,除了为用户提供额外的灵活性之外,它还会强制用户在客户端代码中使用通配符类型。通配符类型对于类的用户来说应该是无形的,它们使方法能够接受它们应该接受的参数,并拒绝那些应该拒绝的参数,如果类的用户必须考虑通配符类型,类的API或许就会出错。
一个经典的例子就是java.uitl.Collections中的copy方法
public static 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
dest.set(i, src.get(i));
} else {
ListIterator super T> di=dest.listIterator();
ListIterator extends T> si=src.listIterator();
for (int i=0; i
di.next();
di.set(si.next());
}
}
}
dest为生产者只从其中取数据,src为消费者,只存放数据进去。
java 协变 逆变_JAVA中的协变与逆变相关推荐
- java协变返回类型_Java中的协变返回类型
java协变返回类型 协变返回类型 (Covariant return type) The covariant return type is that return type which may va ...
- 协变逆变java_Java中的协变与逆变
Java作为面向对象的典型语言,相比于C++而言,对类的继承和派生有着更简洁的设计(比如单根继承). 在继承派生的过程中,是符合Liskov替换原则(LSP)的.LSP总结起来,就一句话: 所有引用基 ...
- java可以多重继承吗_Java中的多重继承与组合vs继承
java可以多重继承吗 有时我写了几篇有关Java继承,接口和组成的文章. 在这篇文章中,我们将研究多重继承,然后了解组成优于继承的好处. Java中的多重继承 多重继承是创建具有多个超类的单个类的能 ...
- java构造器详解_Java中关于构造器的使用详解
这篇文章主要介绍了Java构造器使用方法及注意事项的相关资料,这里举例说明如何使用构造器及需要注意的地方,需要的朋友可以参考下 Java构造器使用方法及注意事项 超类的构造器在子类的构造器运行之前运行 ...
- java 防止sql注入_Java中SQL注入以及如何轻松防止它
java 防止sql注入 什么是SQL注入? (What is SQL Injection?) SQL Injection is one of the top 10 web application v ...
- java 异常处理发生异常_Java中的异常处理
java 异常处理发生异常 Exception Handling in Java is a very interesting topic. Exception is an error event th ...
- java 定义整数数组_JAVA中数组的正确定义方法是什么?
数组是有序数据的集合,数组中的每个元素具有相同的数组名和下标来唯一地确定数组中的元素. §5.1一维数组 一.一维数组的定义 type arrayName[]; 其中类型(type)可以为Java中任 ...
- java中有没有栈_Java中堆和栈有什么区别
stack 和 heep 都是内存的一部分stack 空间小,速度比较快, 用来放对象的引用heep 大,一般所有创建的对象都放在这里.栈(stack):是一个先进后出的数据结构,通常用于保存方法(函 ...
- java 基本类型 引用_java中 引用类型 和 基本类型 有何区别?
栈与堆都是Java用来在Ram中存放数据的地方.与C++不同,Java自动管理栈和堆,程序员不能直接地设置栈或堆. Java的堆是一个运行时数据区,类的(对象从中分配空间.这些对象通过new.newa ...
- java读取文件 路径_Java中的获取文件的物理绝对路径,和读取文件
获取文件的绝对路径,读取该文件 一.文件目录打印图 下面的文件目录图,是项目中文件的位置信息:下面的例子是按照这个图来演示的. . |-- java | |-- ibard | | |-- demo1 ...
最新文章
- 使用正则表达式构造定制的HTML5输入框
- 神操作!一行Python代码搞定一款游戏?给力!
- 如何将本地的项目加入git管理?
- yum配置代理,通过CCProxy有线网络安装软件
- pycharm提示:PEP 8: E127 continuation line over-indented for visual indent
- Vue + Element UI——搜索框DEMO
- c++ 一维数组长度_每天一点C / 一维数组和指针
- ora-07445 oracle 9,Oracle ORA-07445 : 出现异常错误: 核心转储(一)
- pmos导通条件 图示_如何判断NMOS管和PMOS管
- 机器视觉与Tesseract介绍
- 揭示地理数据分布规律的方法
- java 中缀算术表达式转换成后缀表达式_求Java堆栈,将中缀算术表达式转换成后缀表达式。...
- 编码问题,java,当不知道自己的字符串编码是什么的时候,可以用如下程序进行尝试并自动转码utf-8,源码直接可用
- TaskBarProgress(任务栏进度条)
- 小程序 php wecahtpay,PHP 微信公众号,小程序获取支付参数。微信支付
- 从文件中读取一个long型数_Python 从文件中读取数据
- PythonTips1
- java 初始化和清楚_浅谈Java中的初始化和清理
- 『TensorFlow』批处理类
- Google Earth Browser Plugin (谷歌 地球 浏览器 插件) 下载地址 5.0
热门文章
- jQuery实现点击显示和隐藏内容
- webpack和 php配合,javascript - webpack和laravel-elixir-webpack的正确配合方式?
- java age_这些Java9 超牛的新特性,你竟然还没用过?
- 乐迪机器人正确操作_乐迪智能早教机器人好用吗 乐迪智能早教机器人使用测评...
- def __init__(self)是什么意思_一文搞懂什么是Python的metaclass
- dude由于目标计算机,The Dude的教程
- cmd链接php mysql数据库_php连接mysql数据库_cmd连接mysql数据库 - MySQL最新手册教程 - php中文网手册...
- 【持续更新】实用算法小点总结(怕忘QAQ)
- html每访问一次显示1到10,开方表1到100 根号1到10 分别约等于多少
- android图像与动画处理,在Android和iPhone上对照片进行动画处理的7种最佳应用 | MOS86...