一、背景

要搞懂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进阶知识点:协变与逆变的更多相关文章

  1. Java中的协变与逆变

    Java作为面向对象的典型语言,相比于C++而言,对类的继承和派生有着更简洁的设计(比如单根继承). 在继承派生的过程中,是符合Liskov替换原则(LSP)的.LSP总结起来,就一句话: 所有引用基 ...

  2. Java泛型的协变与逆变

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

  3. Java进阶知识点2:看不懂的代码 - 协变与逆变

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

  4. Scala中的协变,逆变,上界,下界等

    Scala中的协变,逆变,上界,下界等 目录 [−] Java中的协变和逆变 Scala的协变 Scala的逆变 下界lower bounds 上界upper bounds 综合协变,逆变,上界,下界 ...

  5. Java用通配符 获得泛型的协变和逆变

    Java对应泛型的协变和逆变

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

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

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

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

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

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

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

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

随机推荐

  1. PL/SQL添加Oracle对象

    1.首先用system的身份进入数据库 2.找到user文件夹 3.右击新建用户 在“创建用户”窗口中,输入新用户的名称.口令,默认表空间.临时表空间等 4.赋予新用户权限,赋予其角色权限:conne ...

  2. unittest单元测试框架之测试用例的跳过(skip) (六)

    1.跳过测试用例的方法 @unittest.skip("don't run this case!"): @unittest.skipIf(3<2,"don't ru ...

  3. Docker 三种UI管理平台

    docker集中化web管理平台 一.shipyard 1.启动docker,下载镜像 # systemctl restart docker # docker pull alpine # docker ...

  4. 获取屏幕翻转:var resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize'

    var resizeEvt = 'orientationchange' in window ? 'orientationchange' : 'resize',这段是为了获取移动端屏幕是否翻转(手机重力 ...

  5. HDU 5536--Chip Factory(暴力)

    Chip Factory Time Limit: 18000/9000 MS (Java/Others)    Memory Limit: 262144/262144 K (Java/Others)T ...

  6. VMware10安装CentOS7

    先去网上下载一个VMware的破解版或者激活版,安装配置这里就不介绍了自行下载安装,基本过程相当于windows下安装个软件而已. CentOS7镜像下载就下阿里云站点的这是链接 http://mir ...

  7. Python提高篇

    Python提高篇 1.模块 1)模块定义 模块就是py文件,可以是你自己写的,也可以是python当中自带的工具,当你在某个py文件下想要引用其他模块的功能,就需要你把你把该py文件导入. 2)导入 ...

  8. PHP Mysql数据库连接

    1,date_default_timezone_set('PRC');//获取北京时区      header("Content-Type:text/html;charset=utf-8&q ...

  9. Currency Helper

    using System; using Microsoft.Xrm.Sdk; using Microsoft.Crm.Sdk.Messages; /// <summary> /// 货币 ...

  10. ES基础知识与高频考点梳理

    知识点梳理目录列表 变量类型 JS的数据类型分类和判断 值类型和引用类型 原型与原型链(继承) 原型和原型链的定义 继承写法 作用域和闭包 执行上下文 this 闭包是什么 异步 同步VS异步 异步和 ...