一、背景

要搞懂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();

List super MochaCoffee> c = new ArrayList();

这三行代码每一行单独看,好像都可以勉强看得懂,但是这三行代码似乎透露出一些让人内心秩序隐隐不安的疑惑:

MochaCoffee[]是Coffee[]的子类?

ArrayList是List extends Coffee>的子类?

ArrayList是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();

List extends MochaCoffee> b = new ArrayList();

a = b; //b可以赋值给a

同样的,虽然ArrayList不是List extends Coffee>的子类,但是List extends X>这种代码模式是协变的,所以b可以直接赋值给a。

值得注意的是,虽然利用协变性,可以将ArrayList赋值给List extends Coffee>,但是赋值后,List extends Coffee>中不能取出MochaCoffee,同时也只能添加null。因为List跟数组不一样,它在运行时插入元素时,类型信息已经被擦除为Object,无法做类型检测,只能依靠声明在编译时做严格的类型检查,List extends Coffee>声明意味着这个容器中的元素类型不确定,可能是Coffee的任何子类,所以往里面添加任何类型都是不安全的,但是可以取出Coffee类型。如下:

List extends Coffee> a = new ArrayList();

//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();

List super Coffee> b = new ArrayList();

a = b; //b可以赋值给a

ArrayList不是List super MochaCoffee>的子类,但是List super X>这种代码模式是逆变的,所以b可以直接赋值给a。

值得注意的是,虽然利用逆变性,可以将ArrayList赋值给List super MochaCoffee>,但是赋值后,List super MochaCoffee>中不能添加Coffee,同时也只能取出Object(除非进行强制类型转换)。List super MochaCoffee>声明意味着这个容器中的元素类型不确定,可能是MochaCoffee的任何基类,所以往里面添加MochaCoffee及其子类是安全的,但是取出的类型就只能是最顶层基类Object了。如下:

List super MochaCoffee> a = new ArrayList();

// a.add(new Coffee()); //不能添加Coffee

a.add(new MochaCoffee()); //可以添加MochaCoffee

Object object = a.get(0); //只能取出Object

注:没有extend和super关键字加持的泛型模式都是不变的,A与B之间有继承关系,但是List和List之间不享受任何继承待遇,这就解决了上面提到数组协变导致的问题,让类型错误在编译时就可以被发现。

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进阶知识点:协变与逆变相关推荐

  1. java协变 生产者理解_Linux 大规模请求服务器连接数相关设置

    一般一个大规模Linux服务器请求数可能是几十万上百万的情况,需要足够的连接数来使用,所以务必进行相应的设置. 默认的Linux服务器文件描述符等打开最大是1024,用ulimit -a 查看: [v ...

  2. java协变返回类型_Java中的协变返回类型

    java协变返回类型 协变返回类型 (Covariant return type) The covariant return type is that return type which may va ...

  3. java inputstream理解_Java进阶核心之InputStream流深入讲解

    Java核心包 java.io包介绍 IO: Input / Ouput 即输入输出 输出流:程序(内存) ->外界设备 输入流:外界设备->程序(内存) 处理理数据类型分类 字符流:处理 ...

  4. java全局变量怎么定义_Java开发知识点:如何理解Java函数式编程?

    Java是一种计算机编程语言,可用于编写桌面应用程序.Web应用程序.分布式系统和嵌入式系统应用程序等,是IT开发行业中最受欢迎的编程语言之一.想要学好Java必须要一步一个脚印打好基础.积攒实战经验 ...

  5. java arraylist 实现原理_Java进阶--深入理解ArrayList实现原理

    编辑推荐: 文章主要介绍ArrayList的继承关系,ArrayList的方法使用和源码解析,它提供了动态的增加和减少元素,实现了Collection和List接口,可以灵活的设置数组的大小,希望对您 ...

  6. Java设计流程执行器_Java进阶面试精选系列:SpringMVC+SpringBoot+Hibernate+Mybatis+设计模式...

    小编精心收集:为金三银四准备,以下面试题先过一遍,为即将到了的面试做好准备,也过一遍基础知识点. 一.Spring/Spring MVC 1.为什么要使用 spring? 2.解释一下什么是 aop? ...

  7. java面向对象的理解_java胜于C语言,却又静态面向对象,简单?

    Java是一门面向对象编程语言,不仅吸收了C++语言的各种优点,还摒弃了C++里难以理解的多继承.指针等概念,因此Java语言具有功能强大和简单易用两个特征. Java语言作为静态面向对象编程语言的代 ...

  8. java多线程 生产者消费者_java多线程之-生产者与消费者

    java多线程之-并发协作[生产者与消费者]模型 对于多线程程序来说,不管c/c++ java python 等任何编程语言,生产者与消费者模型都是最为经典的.也就是可以说多线程的并发协作 对于此模型 ...

  9. java抽象的理解_Java中抽象类的理解

    Java中抽象类的理解 创建   所有   bsp   抽象类   member   初始   dem   print ---------------------------------------- ...

最新文章

  1. 用cascade删除有约束的表或记录
  2. linux自定义和使用 shell 环境(一)
  3. python数据结构的应用场景不包括,Python 数据结构学习
  4. 织梦cms第四版仿七猫技术导航源码 附安装教程
  5. 国外机器人产业发展经验
  6. 大话Synchronized及锁升级
  7. 等差数列java_Java实现 LeetCode 413 等差数列划分
  8. Sharepoint2013:在页面上显示错误信息
  9. js-事件处理(重点)
  10. 十个免费的 Web 压力测试工具
  11. VM VirtualBox安装mac os dmg 转 iso
  12. vue中使用vue-baidu-map 实现点 弹窗 路线 行政区划分
  13. ictclas4j java_Paoding, Ik, Jeasy, Ictclas4j分词工具
  14. 解析G652,G657A,G655和G654光缆之间的区别
  15. 使用Python来调教我的微信
  16. 在浏览器进行大文件分片上传(java服务端实现)
  17. html表格怎么控制文字大小,如何用html设置文本输入框输入字体的大小
  18. Notepad++介绍与安装
  19. 卫生健康信息生态体系的关键技术应用摘要
  20. 漂亮的蓝色系网站设计欣赏1

热门文章

  1. 【笔记】buck/boost/buck-boost相关计算公式
  2. 开源工作流程引擎ccflow多人待办处理模式的详解
  3. 业内首创普惠保险,看国泰产险如何借助数据进行智能化的升级和战略转型
  4. Bitbucket Pipelines在Atlassian的Bitbucket云上提供持续交付功能
  5. Position和anchorPoint
  6. innobackupex --rsync 报错 Error: can't create file (null)/xtrabackup_rsyncfiles_pass1
  7. YUDBModel【绿色插件】-对象序列化、反序列化、对象一键增删改查
  8. nyoj 47 江 河问题 【贪婪】
  9. DHCP详细工作过程
  10. stm32 堆和栈(stm32 Heap Stack)【worldsing笔记】