Java进阶知识点2:看不懂的代码 - 协变与逆变
一、背景
要搞懂Java中的协办与逆变,不得不从继承说起,如果没有继承,协变与逆变也天然不存在了。
我们知道,在Java的世界中,存在继承机制。比如MochaCoffee类是Coffee类的派生类,那么我们可以在任何时候使用MochaCoffee类的引用去替换Coffee类的引用(重写函数时,形参必须与重写函数完全一致,这是一处列外),而不会引发编译错误(至于会不会引发程序功能错误,取决于代码是否符合里氏替换原则)。
简而言之,如果B类是A类的派生类,那么B类的引用可以赋值给A类的引用。
赋值的方式最常见有两种。
第一:使用等于运算符显式赋值
Coffee coffee = new MochaCoffee();
上述代码可以分两阶段理解,首先new MochaCoffee()返回MochaCoffee的引用,然后将此引用显式赋值给Coffee类型的引用。
第二:函数传参赋值
public class Main {
public static void main(String[] args) {
function(new MochaCoffee());
}
public static void function(Coffee coffee) {
}
}
基础知识复习完后,我们正式开始进入协变与逆变的世界,首先我们来看如下常见代码:
Coffee a[] = new MochaCoffee[10];
List<? extends Coffee> b = new ArrayList<MochaCoffee>();
List<? super MochaCoffee> c = new ArrayList<Coffee>();
这三行代码每一行单独看,好像都可以勉强看得懂,但是这三行代码似乎透露出一些让人内心秩序隐隐不安的疑惑:
MochaCoffee[]是Coffee[]的子类?
ArrayList<MochaCoffee>是List<? extends Coffee>的子类?
ArrayList<Coffee>是List<? super MochaCoffee>的子类?
我们只学习过Class之间有继承关系,这些数组、容器类型之间难道也有继承关系,这种继承关系在JDK哪一处源码中有定义?还有没有其他类似的情况?
如果你也有类似的问题,说明你的知识体系中缺失了一个知识点,这就是我们今天讲的Java中的协变与逆变。
二、逆变与协变
2.1 定义
假设F(X)代表Java中的一种代码模式,其中X为此模式中可变的部分。如果B是A的派生类,而F(B)也享受F(A)派生类的待遇,那么F模式是协变的,如果F(A)反过来享受F(B)派生类的待遇,那么F模式是逆变的。如果F(A)和F(B)之间不享受任何继承待遇,那么F模式是不变的。(这里的继承待遇指的是前面复习到的“如果B类是A类的派生类,那么B类的引用可以赋值给A类的引用。”)
Java中绝大部分代码模式都是不变的(大家可以安心了)。
2.2 Java中的协变与协变模式
Java中目前已知的支持协变与逆变的模式,我总结了三类,欢迎大家补充。
2.2.1 F(X) = 将X数组化,此时F模式是协变的
Coffee a[] = new Coffee[10];
MochaCoffee b[] = new MochaCoffee[10];
a = b; //b可以赋值给a
这可以回答之前的问题,虽然MochaCoffee[]不是Coffee[]的子类,但数组化这种代码模式是协变的,所以MochaCoffee[]也可以直接赋值给Coffee[]。
值得注意的是,虽然数组是协变的,但是数组是会记住实际类型并在每一次往数组中添加元素时做类型检查。比如如下代码虽然可以利用数组的协变性通过编译,但是运行时依然会抛出异常。
Coffee a[] = new MochaCoffee[10];
a[0] = new Coffee(); //抛出ArrayStoreException
这也是数组的协变设计被广为诟病的原因,因为异常应该尽量在编译时就发现,而不是推迟到运行时。不过数组支持协变后,java.util.Arrays#equals(java.lang.Object[], java.lang.Object[])这种类型的函数就不需要为每种可能的数组类型去分别实现一次了。数组的协变设计有历史版本兼容性方面的考虑等,Java的每一个设计可能不是最优的,但确实是设计者在当时的情况下可以做出的最好选择。
2.2.2 F(X) = 将X通过<? extends X>语法作为泛型参数,此时F模式是协变的
List<? extends Coffee> a = new ArrayList<MochaCoffee>();
List<? extends MochaCoffee> b = new ArrayList<MochaCoffee>();
a = b; //b可以赋值给a
同样的,虽然ArrayList<MochaCoffee>不是List<? extends Coffee>的子类,但是List<? extends X>这种代码模式是协变的,所以b可以直接赋值给a。
值得注意的是,虽然利用协变性,可以将ArrayList<MochaCoffee>赋值给List<? extends Coffee>,但是赋值后,List<? extends Coffee>中不能取出MochaCoffee,同时也只能添加null。因为List跟数组不一样,它在运行时插入元素时,类型信息已经被擦除为Object,无法做类型检测,只能依靠声明在编译时做严格的类型检查,List<? extends Coffee>声明意味着这个容器中的元素类型不确定,可能是Coffee的任何子类,所以往里面添加任何类型都是不安全的,但是可以取出Coffee类型。如下:
List<? extends Coffee> a = new ArrayList<MochaCoffee>();
//a.add(new MochaCoffee()); //不能添加MochaCoffee
//a.add(new Coffee()); //也不能添加Coffee
a.add(null); //可以添加null
Coffee coffee = a.get(0); //可以取出Coffee
2.2.3 F(X) = 将X通过<? super X>语法作为泛型参数,此时F模式是逆变的
List<? super MochaCoffee> a = new ArrayList<Coffee>();
List<? super Coffee> b = new ArrayList<Coffee>();
a = b; //b可以赋值给a
ArrayList<Coffee>不是List<? super MochaCoffee>的子类,但是List<? super X>这种代码模式是逆变的,所以b可以直接赋值给a。
值得注意的是,虽然利用逆变性,可以将ArrayList<Coffee>赋值给List<? super MochaCoffee>,但是赋值后,List<? super MochaCoffee>中不能添加Coffee,同时也只能取出Object(除非进行强制类型转换)。List<? super MochaCoffee>声明意味着这个容器中的元素类型不确定,可能是MochaCoffee的任何基类,所以往里面添加MochaCoffee及其子类是安全的,但是取出的类型就只能是最顶层基类Object了。如下:
List<? super MochaCoffee> a = new ArrayList<Coffee>();
// a.add(new Coffee()); //不能添加Coffee
a.add(new MochaCoffee()); //可以添加MochaCoffee
Object object = a.get(0); //只能取出Object
注:没有extend和super关键字加持的泛型模式都是不变的,A与B之间有继承关系,但是List<A>和List<B>之间不享受任何继承待遇,这就解决了上面提到数组协变导致的问题,让类型错误在编译时就可以被发现。
2.3 PECS原则
2.2.2和2.2.3中的注意事项,也体现了著名的PECS原则:“Producer Extends,Consumer Super”。
因为使用<? extends T>后,如果泛型参数作为返回值,用T接收一定是安全的,也就是说使用这个函数的人可以知道你生产了什么东西;
而使用<? super T>后,如果泛型参数作为入参,传递T及其子类一定是安全的,也就是说使用这个函数的人可以知道你需要什么东西来进行消费。
比如Java8新增的函数接口java.util.function.Consumer#andThen方法就体现了Consumer Super这一原则。
三、总结
1、数组是协变的。
2、extends关键字加持的泛型是协变的。
3、super关键字加持的泛型是逆变的。
4、注意数组和泛型容器中添加和获取元素的类型限制。
Java进阶知识点2:看不懂的代码 - 协变与逆变的更多相关文章
- Java进阶知识点: 枚举值
Java进阶知识点1:白捡的扩展性 - 枚举值也是对象 一.背景 枚举经常被大家用来储存一组有限个数的候选常量.比如下面定义了一组常见数据库类型: public enum DatabaseType ...
- Java进阶知识点:协变与逆变
一.背景 要搞懂Java中的协办与逆变,不得不从继承说起,如果没有继承,协变与逆变也天然不存在了. 我们知道,在Java的世界中,存在继承机制.比如MochaCoffee类是Coffee类的派生类,那 ...
- [改善Java代码]警惕泛型是不能协变和逆变的
什么叫做协变(covariance)和逆变(contravariance)? 在变成语言的类型框架中,协变和逆变是指宽类型和窄类型在某种情况下(如参数,泛型,返回值)替换或交换的特性,简单的说,协变是 ...
- Java的协变、逆变与不可变
package javase; import java.util.ArrayList; import java.util.List; class Animal{ } class Cat extends ...
- Java语言中的协变和逆变(zz)
转载声明: 本文转载至:http://swiftlet.net/archives/1950 协变和逆变指的是宽类型和窄类型在某种情况下的替换或交换的特性.简单的说,协变就是用一个窄类型替代宽类型,而逆 ...
- Java泛型中的协变和逆变
Java泛型中的协变和逆变 一般我们看Java泛型好像是不支持协变或逆变的,比如前面提到的List<Object>和List<String>之间是不可变的.但当我们在Java泛 ...
- Java泛型的协变与逆变
泛型擦除 Java的泛型本质上不是真正的泛型,而是利用了类型擦除(type erasure),比如下面的代码就会出现错误: 报的错误是:both methods have same erasure ...
- Java协变、逆变、类型擦除
协变.逆变 定义 Java中String类型是继承自Object的,姑且记做String ≦ Object,表示String是Object的子类型,String的对象可以赋给Object的对象.而Ob ...
- 那些年搞不懂的"协变"和"逆变"
博主之前也不是很清楚协变与逆变,今天在书上看到了有关于协变还是逆变的介绍感觉还是不太懂,后来看了一篇园子里面一位朋友的文章,顿时茅塞顿开.本文里面会有自己的一些见解也会引用博友的一些正文,希望通过本篇 ...
随机推荐
- 财经世界(2)A股B股和H股
在发行过程中,公司通过章程给不同股份形式赋予不同的权益,使公司股份出现A股.B股等形式.我国上市公司的股票有A股.B股.H股.N股.S股等的区分.这一区分的主要依据股票的上市地点和所面对的投资者而定. ...
- windows通过ftp下载linux文件
# windows 下载 linux的文件>> ftp <domain_or_ip>>> <input_username>>> <in ...
- 【CodeChef】Small factorials(BigInteger笔记)
You are asked to calculate factorials of some small positive integers. Input An integer t, 1<=t&l ...
- Python编程-多进程一
一.python并发编程之多进程 1.multiprocessing模块介绍 python中的多线程无法利用多核优势,如果想要充分地使用多核CPU的资源(os.cpu_count()查看),在pyth ...
- 深入理解Node.js中的垃圾回收和内存泄漏的捕获
深入理解Node.js中的垃圾回收和内存泄漏的捕获 文章来自:http://wwsun.github.io/posts/understanding-nodejs-gc.html Jan 5, 2016 ...
- JMeter学习(八)JDBC Request
[step_1]:“测试计划”--(右键)à添加à线程组: [step_2]:选择step_1中添加的线程组—(右键)à添加à配置元件àJDBC Connection Configuration,添加 ...
- POJ 1459 网络流 EK算法
题意: 2 1 1 2 (0,1)20 (1,0)10 (0)15 (1)20 2 1 1 2 表示 共有2个节点,生产能量的点1个,消耗能量的点1个, 传递能量的通道2条:(0,1)20 (1,0) ...
- UVA 11186 Circum Triangle (枚举三角形优化)(转)
题意:圆上有n个点,求出这n个点组成的所有三角形的面积之和 题解: 当我们要求出S(i,j,k)时,我们需要假设k在j的左侧,k在i与j之间,k在i的右侧. 如果k在 j的左侧 那么 S(i,j,k ...
- BASE64Encoded() 方法报错说方法未定义
代码: String enParams = new BASE64Encoder().encode(strParams.getBytes()); 出错,显示方法未定义 解决方法:项目右键——>pr ...
- linux下如何查看某个软件 是否安装??? 安装路径在哪???
<1>.在linux下如何判断是否已经安装某个软件? 我的系统是red hat 我用命令rpm查是否安装了某个软件(例如: matlab 与 fluent ),得到了如下的结果: 我用rp ...