Java进阶知识点:协变与逆变
一、背景
要搞懂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通过<? extend 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、extend关键字加持的泛型是协变的。
3、super关键字加持的泛型是逆变的。
4、注意数组和泛型容器中添加和获取元素的类型限制。
Java进阶知识点:协变与逆变的更多相关文章
- Java中的协变与逆变
Java作为面向对象的典型语言,相比于C++而言,对类的继承和派生有着更简洁的设计(比如单根继承). 在继承派生的过程中,是符合Liskov替换原则(LSP)的.LSP总结起来,就一句话: 所有引用基 ...
- Java泛型的协变与逆变
泛型擦除 Java的泛型本质上不是真正的泛型,而是利用了类型擦除(type erasure),比如下面的代码就会出现错误: 报的错误是:both methods have same erasure ...
- Java进阶知识点2:看不懂的代码 - 协变与逆变
一.背景 要搞懂Java中的协办与逆变,不得不从继承说起,如果没有继承,协变与逆变也天然不存在了. 我们知道,在Java的世界中,存在继承机制.比如MochaCoffee类是Coffee类的派生类,那 ...
- Scala中的协变,逆变,上界,下界等
Scala中的协变,逆变,上界,下界等 目录 [−] Java中的协变和逆变 Scala的协变 Scala的逆变 下界lower bounds 上界upper bounds 综合协变,逆变,上界,下界 ...
- Java用通配符 获得泛型的协变和逆变
Java对应泛型的协变和逆变
- [改善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泛 ...
随机推荐
- ASP.NET Core 2.0中如何更改Http请求的maxAllowedContentLength最大值
Web.config中的maxAllowedContentLength这个属性可以用来设置Http的Post类型请求可以提交的最大数据量,超过这个数据量的Http请求ASP.NET Core会拒绝并报 ...
- 绘图驱动-OSD原理2
转载自:http://blog.pfan.cn/programming/21209.html 现在已经可以通过修改存储单元内容来改变OSD的像素,但还有个关键的问题是如何根据需要来进行操作,即如何将某 ...
- easyui 上 datagrid 的表头的checkbox全选时 取消选中 disabled的checkbox
业务需求: 正常情况下,easyui的全选checkbox会选择表中全部的checkbox包括行.及时对checkbox加了disable属性也没有效果.但是现在的业务是当对checkbox加了dis ...
- 小K的农场(差分约束,spfa)
题目描述 小K在MC里面建立很多很多的农场,总共n个,以至于他自己都忘记了每个农场中种植作物的具体数量了,他只记得一些含糊的信息(共m个),以下列三种形式描述: 农场a比农场b至少多种植了c个单位的作 ...
- 竞赛题解 - NOIP2018 旅行
\(\mathcal {NOIP2018} 旅行 - 竞赛题解\) 坑还得一层一层的填 填到Day2T1了 洛谷 P5022 题目 (以下copy自洛谷,有删减/修改 (●ˇ∀ˇ●)) 题目描述 小 ...
- iOS 打包常见问题处理
Cannot proceed with delivery: an existing transporter instance is currently uploading this package 原 ...
- python的基本知识
1. python的简介 python的创始⼈人为吉多·范罗苏姆(Guido van Rossum).1989年年的圣诞节期间,吉多· 范罗苏姆为了了在阿姆斯特丹丹打发时间,决⼼心开发⼀个新的脚 ...
- scala 获取当前时间的两种方式
在编写程序时,有时需要获取当前时间,这在记录异常信息.获取程序运行耗时很有用处 方式一: val time1=System.currentTimeMillis() 这种方式获取的是程序运行到此的毫秒数 ...
- Python环境搭建(Windows)
·Python环境搭建(Windows) @ 下载Python Python官网:https://www.python.org/ Python帮助文档下载地址:https://www.python ...
- 计算机基础和Linux基础
计算机原理 计算机发展史 机器语言—让机器干活 差分机—让机器的数学运算和逻辑运算只简化成“加法”,计算机只处理“加法” 计算机硬件CPU=运算器+控制器+寄存器(缓存)硬盘=存储器+寄存器寄存器是为 ...