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 ...
- 那些年搞不懂的"协变"和"逆变"
博主之前也不是很清楚协变与逆变,今天在书上看到了有关于协变还是逆变的介绍感觉还是不太懂,后来看了一篇园子里面一位朋友的文章,顿时茅塞顿开.本文里面会有自己的一些见解也会引用博友的一些正文,希望通过本篇 ...
随机推荐
- Android零散知识点积累
本文仅在记录android开发中遇到的零散知识点,会不断更新... 目录 .隐藏系统标题栏 .图片尺寸及屏幕密度 3.获取顶部状态栏高度 1.隐藏系统标题栏 1)在资源文件styles.xml中定义样 ...
- XSS - html过滤
JS 根据白名单过滤HTML http://jsxss.com/zh/index.html 方案一: java的一个方案, 可以参考: http://winnie825.iteye.com/bl ...
- 谷歌机器学习速成课程---降低损失 (Reducing Loss):随机梯度下降法
在梯度下降法中,批量指的是用于在单次迭代中计算梯度的样本总数.到目前为止,我们一直假定批量是指整个数据集.就 Google 的规模而言,数据集通常包含数十亿甚至数千亿个样本.此外,Google 数据集 ...
- range精讲
var ec = range.endContainer endContainer不是一个引用类型 range是引用类型 range经过改变范围之后 var ec2 =range.endContaine ...
- HackerRank - fibonacci-modified 【大数】
思路 用PYTHON 或 JAVA 干掉 AC代码 a, b, n = map(int, input().split()) for i in range (2, n, 1) : temp = b b ...
- C# partial 关键字
C# partial关键字详解 partial关键字允许把类.结构或接口放在多个文件中.一般情况下,一个类存储在单个文件中.但有时,多个开发人员需要访问同一个类,或者某种类型的代码生成器生成了一个类的 ...
- 020_自己编写的wordcount程序在hadoop上面运行,不使用插件hadoop-eclipse-plugin-1.2.1.jar
1.Eclipse中无插件运行MP程序 1)在Eclipse中编写MapReduce程序 2)打包成jar包 3)使用FTP工具,上传jar到hadoop 集群环境 4)运行 2.具体步骤 说明:该程 ...
- mysql 触发器 存储过程 java调用
触发器和存储过程是为了提高SQL的运行效率. SQL语句先编译.后执行,而触发器与存储过程都会提前预编译完成,且只编译一次,供反复调用. 随着时代的进步,硬件与带宽的提升,触发器和存储过程提升效率并不 ...
- 【Head First Servlets and JSP】笔记19:JavaBeans与JSP动作元素(<jsp:setProperty.....>、<jsp:getProperty.....>)
内容来自imooc. 1.什么是JSP动作元素 2.在JSP页面中如何使用Javabeans <jsp:......>表示这是一个JSP动作元素 3.使用JSP动作元素创建JavaBean ...
- 摊铺机基本参数介绍(三一重工SSP220C-5)
三一重工SSP220C-5稳定土摊铺机参数 SSP系列稳定土摊铺机SSP220C-5 动力强劲162kw 动力充分满足摊铺机各种工况下动力需求 高效任何工况,确保摊铺能力大于900t/h,行业内绝无仅 ...