一、背景

要搞懂Java中的协办与逆变,不得不从继承说起,如果没有继承,协变与逆变也天然不存在了

我们知道,在Java的世界中,存在继承机制。比如MochaCoffee类是Coffee类的派生类,那么我们可以在任何时候使用MochaCoffee类的引用去替换Coffee类的引用(重写函数时,形参必须与重写函数完全一致,这是一处列外),而不会引发编译错误(至于会不会引发程序功能错误,取决于代码是否符合里氏替换原则)。

简而言之,如果B类是A类的派生类,那么B类的引用可以赋值给A类的引用

赋值的方式最常见有两种。

第一:使用等于运算符显式赋值

  1. Coffee coffee = new MochaCoffee();

上述代码可以分两阶段理解,首先new MochaCoffee()返回MochaCoffee的引用,然后将此引用显式赋值给Coffee类型的引用。

第二:函数传参赋值

  1. public class Main {
  2. public static void main(String[] args) {
  3. function(new MochaCoffee());
  4. }
  5.  
  6. public static void function(Coffee coffee) {
  7. }
  8. }

基础知识复习完后,我们正式开始进入协变与逆变的世界,首先我们来看如下常见代码:

  1. Coffee a[] = new MochaCoffee[10];
  2. List<? extends Coffee> b = new ArrayList<MochaCoffee>();
  3. 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模式是协变的

  1. Coffee a[] = new Coffee[10];
  2. MochaCoffee b[] = new MochaCoffee[10];
  3. a = b; //b可以赋值给a

这可以回答之前的问题,虽然MochaCoffee[]不是Coffee[]的子类,但数组化这种代码模式是协变的,所以MochaCoffee[]也可以直接赋值给Coffee[]。

值得注意的是,虽然数组是协变的,但是数组是会记住实际类型并在每一次往数组中添加元素时做类型检查。比如如下代码虽然可以利用数组的协变性通过编译,但是运行时依然会抛出异常。

  1. Coffee a[] = new MochaCoffee[10];
  2. 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模式是协变的

  1. List<? extends Coffee> a = new ArrayList<MochaCoffee>();
  2. List<? extends MochaCoffee> b = new ArrayList<MochaCoffee>();
  3. 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类型。如下:

  1. List<? extends Coffee> a = new ArrayList<MochaCoffee>();
  2. //a.add(new MochaCoffee()); //不能添加MochaCoffee
  3. //a.add(new Coffee()); //也不能添加Coffee
  4. a.add(null); //可以添加null
  5. Coffee coffee = a.get(0); //可以取出Coffee

2.2.3 F(X) = 将X通过<? super X>语法作为泛型参数,此时F模式是逆变的

  1. List<? super MochaCoffee> a = new ArrayList<Coffee>();
  2. List<? super Coffee> b = new ArrayList<Coffee>();
  3. 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了。如下:

  1. List<? super MochaCoffee> a = new ArrayList<Coffee>();
  2. // a.add(new Coffee()); //不能添加Coffee
  3. a.add(new MochaCoffee()); //可以添加MochaCoffee
  4. 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. iOS:文件操作相关(18-03-23更)

    0.iOS文件系统 1.工程内文件 2.文件夹管理 3.文件操作 4.NSCache 附录: 1.沙盒文件夹.文件大小 2.清除沙盒 Library / Cache 下所有数据 3.测试plist 0 ...

  2. Spring注解配置(1)——@Autowired

    @Autowired 注释,它可以对类成员变量.方法及构造函数进行标注,完成自动装配的工作. 通过 @Autowired的使用来消除 set ,get方法.在使用@Autowired之前,我们对一个b ...

  3. NPOI读取Excel遇到的坑

    NPOI是POI的.NET版本.POI是用Java写成的库,能帮助用户在没有安装Office环境下读取Office2003-2007文件.NPOI在.NET环境下使用,能读写Excel/Word文件. ...

  4. Linux 学习第四天

    Linux学习第四天 一.常用命令 1.tar  (压缩.解压) A.添加压缩包  tar czvf 压缩包名称.tar.gz 源文件 B.添加压缩包  tar cjvf 压缩包名称.tar.bz2 ...

  5. 转:30分钟学会如何使用Shiro

    引自:http://www.cnblogs.com/learnhow/p/5694876.html 本篇内容大多总结自张开涛的<跟我学Shiro>原文地址:http://jinniansh ...

  6. 纯JS实现轮播图特效——详解

    <div id="slider"> <div id="sliderImgs"> <img src="img/mi04.j ...

  7. Spring : JPA的单独使用

    title: 如何单独使用spring data jpa 引用pom文件: <dependency> <groupId>org.springframework.data< ...

  8. 虚拟机搭建hadoop的步骤

    1.首先是安装Vmware Workstation,下载地址:https://www.vmware.com/products/workstation-player/workstation-player ...

  9. HyperLedger Fabric 1.4 单机单节点部署(10.2)

    单机单节点指在一台电脑上部署一个排序(Orderer)服务.一个组织(Org1),一个节点(Peer,属于Org1),然后运行官方案例中的example02智能合约例子,实现转财交易和查询功能.单机单 ...

  10. MDK/Keil 中,J-Link调试查看变量值总是显示<not in scope>

    转载请注明出处,谢谢. MDK/Keil 中,J-Link调试查看变量值总是显示<not in scope> 原因:编译器把代码优化掉了,直接导致在仿真中变量根本没有分配内存,也就无法查看 ...