一、背景

要搞懂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:看不懂的代码 - 协变与逆变的更多相关文章

  1. Java进阶知识点: 枚举值

    Java进阶知识点1:白捡的扩展性 - 枚举值也是对象   一.背景 枚举经常被大家用来储存一组有限个数的候选常量.比如下面定义了一组常见数据库类型: public enum DatabaseType ...

  2. Java进阶知识点:协变与逆变

    一.背景 要搞懂Java中的协办与逆变,不得不从继承说起,如果没有继承,协变与逆变也天然不存在了. 我们知道,在Java的世界中,存在继承机制.比如MochaCoffee类是Coffee类的派生类,那 ...

  3. [改善Java代码]警惕泛型是不能协变和逆变的

    什么叫做协变(covariance)和逆变(contravariance)? 在变成语言的类型框架中,协变和逆变是指宽类型和窄类型在某种情况下(如参数,泛型,返回值)替换或交换的特性,简单的说,协变是 ...

  4. Java的协变、逆变与不可变

    package javase; import java.util.ArrayList; import java.util.List; class Animal{ } class Cat extends ...

  5. Java语言中的协变和逆变(zz)

    转载声明: 本文转载至:http://swiftlet.net/archives/1950 协变和逆变指的是宽类型和窄类型在某种情况下的替换或交换的特性.简单的说,协变就是用一个窄类型替代宽类型,而逆 ...

  6. Java泛型中的协变和逆变

    Java泛型中的协变和逆变 一般我们看Java泛型好像是不支持协变或逆变的,比如前面提到的List<Object>和List<String>之间是不可变的.但当我们在Java泛 ...

  7. Java泛型的协变与逆变

    泛型擦除 Java的泛型本质上不是真正的泛型,而是利用了类型擦除(type erasure),比如下面的代码就会出现错误: 报的错误是:both methods  have same erasure ...

  8. Java协变、逆变、类型擦除

    协变.逆变 定义 Java中String类型是继承自Object的,姑且记做String ≦ Object,表示String是Object的子类型,String的对象可以赋给Object的对象.而Ob ...

  9. 那些年搞不懂的"协变"和"逆变"

    博主之前也不是很清楚协变与逆变,今天在书上看到了有关于协变还是逆变的介绍感觉还是不太懂,后来看了一篇园子里面一位朋友的文章,顿时茅塞顿开.本文里面会有自己的一些见解也会引用博友的一些正文,希望通过本篇 ...

随机推荐

  1. 使用C# .NET 将结构数组绑定到 Windows 窗体的方法

      本任务的内容 概要 要求 设计结构 向数组添加结构实例 将结构成员绑定到窗体控件 提供浏览数组的方式 分步示例 参考 概要 本文介绍如何向 Windows 窗体绑定结构数组. 该示例由一个 Win ...

  2. 在运行myeclipse10注册机时,显示找不到com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel这个包

    在win7下安装MyEclipse10.安装完成之后运行注册机,总是提示classnotfond显示找不到com.sun.java.swing.plaf.nimbus.NimbusLookAndFee ...

  3. idea java web 使用说明

    String realPath = request.getSession().getServletContext().getRealPath(uploadPath);//上传压缩包所在目录       ...

  4. 运行docker image 忘记添加端口号

    docer inspect  容器id,查找IpAddress ,通过这个访问

  5. iOS 视频全屏功能 学习

    项目中,也写过类似"视频全屏"的功能, 前一阵子读到今日头条 的一篇技术文章,详细介绍三种旋转方法差异优劣最终择取.文章从技术角度看写的非常好,从用户角度看,也用过多家有视频功能的 ...

  6. OpenGL学习进程(3)第一课:初始化窗体

        本节是OpenGL学习的第一个课时,下面介绍如何初始化一个窗体:     (1)显示一个有蓝色背景的窗体: #include <GL/glut.h> #include <st ...

  7. 建议44:理解模块pickle优劣

    # -*- coding:utf-8 -*- ''' pickle 估计是最通用的序列化模块了,它还有个C 语言的实现cPickle,相比pickle 来说 具有较好的性能,其速度大概是pickle ...

  8. VMware 连接不上XSHELL

    本人创建虚拟机时把网络连接模式选成了桥接,后来在VMware虚拟网络编辑器中查看了连接模式:NAT. 重新回到VMware中更改了连接模式:NAT模式 成功连接XSHELL 步骤1 打开VMware的 ...

  9. oracle 启动数据库与监听器

    1.oracle 启动数据库与监听器 1)启动数据库 oracle用户进去 oracle/oracle sqlplus / as sysdba 然后startup 退出,然后启动监听进程 2)启动监听 ...

  10. jq点击切换按钮最简洁代码

    <div id="swphoto">    <img src="1.jpg">    <img src="2.jpg&q ...