Java作为面向对象的典型语言,相比于C++而言,对类的继承和派生有着更简洁的设计(比如单根继承)。

  在继承派生的过程中,是符合Liskov替换原则(LSP)的。LSP总结起来,就一句话:

    所有引用基类(父类)的地方必须能够透明地使用其子类的对象。

  LSP包含四层含义:

    ① 子类完全拥有父类的方法,且具体子类必须实现父类的抽象方法;

    ② 子类中可以增加自己的方法;

    ③ 当子类覆盖或实现父类的方法时,方法的形参要比父类方法的更加宽松;

    ④ 当子类覆盖或实现父类的方法时,方法的返回值要比父类方法的更加严格。

  针对LSP四层含义的③④条,就引出了协变(Covariance)和逆变(Contravariance)的概念:

    协变,简言之,就是父类型到子类型,变得越来越具体,在Java中体现在返回值类型不变或更加具体(异常类型也是如此)等。

    逆变,简言之,就是父类型到子类型,变得越来越具体,但是方法的形参却变得更加抽象或不变(注意:这里在Java中本质为方法重载,而不是覆盖,当添加@Override标签将会报错!)

  于是,针对上述的逆变,概念可以理解,但是Java中所谓的“方法形参变得更加宽泛”,实质是方法重载,似乎也就不能严格地称为继承关系下的“逆变”,思考之,似乎Java的继承派生过程中,几乎所有的操作,都是协变的,那么是不是就说明Java中并不存在逆变呢???

  错!Java中是存在逆变的!

  这里就引出了Java中的泛型,这里举个例子:

  在Java中,Number类是Integer类的父类(super),如果某个方法的签名是void method(List<Number> listNumber),那么按照协变的思想,是不是意味着为这个方法传入List<Integer>类型参数也是可以的呢?

  当然不... 很多博客在此都说“Java对于这样的泛型是不支持协变的”,但我认为,事实是List<Integer>的实参,依旧是一个List类型的持有对象,因此对于List<Number>这个持有对象来说,二者持有的对象存在继承派生的关系,但二者本身并不存在继承派生的关系,因而也就无从谈及协变(实质,这里二者的关系是“不变”,“不变”是针对于协变与逆变概念而言的)。

  举个例子来说明这个简单的问题,一个父亲和他的儿子都分别有一辆车,他们的车款型相同(当然,也可能不同,但总归是车,即持有对象,这里为了针对上述二者持有对象均为List故意言之),尽管车上的父亲和儿子存在着“继承派生”的关系,但是这两辆车并不存在继承关系,所以二者之间并没有“协变”的概念。

  那么,总不能对于这样的method,要为每种持有对象持有的对象类型分别重载实现method吧...于是,Java就提供了泛型的通配符(注意,这里才谈到泛型),为了解决上述的method问题,可以这样声明method: void method(List<? extends Number> listNumber)。这样,这里的形参就必须是一个持有对象,它持有的对象类型,必须是Number类或者是继承自Number类的更具体的子类(如Integer类,Double类),此时,可以说这个方法依旧实现了“协变”,那么Java中的逆变是体现在哪儿的呢?

  这里就引出了通配符后另一个关键字,super。

  这样声明的方法:void method(List<? super Integer> listInteger),说明该持有对象持有的对象类型,必须是Integer或Integer的父类(超类super),于是,此时向方法中传递持有Number类的持有对象也是可以的,甚至,可以传递一个持有Object类型的持有对象。此处便是使得参数类型变得更加宽泛,因此此处体现的是“逆变”。

  这也很好记:

    ? extends 对应 协变

    ? super    对应 逆变

    (? 即为Java泛型的通配符)

  综上,Java是符合LSP的一门语言,对“协变”“逆变”的支持也是有具体实现以及道理的。理解好这些概念,可以让编程中遇到的知识概念更加系统化,理解记忆也更高效。

  至于前几天,有同学在群里讨论,Java中如果子类覆盖了父类的方法,是否就不符合LSP了,如果说Java是严格按照LSP来设计的,那么这种情况是否就不能称为覆盖,而是重载...

  当时被这个问题雷到了... 我理解的LSP应该是一种思想,是设计过程以及实现过程中开发者应该牢记并遵守的。如果按照LSP的总的规则,那么每个父类对象出现的地方,都可以用其具体子类对象来替换而不会发生错误。这个错误当然是保证语法编译不会发生错误,而不是针对覆盖方法导致的功能不同。所以...这个问题,实质上应该归结于理解发生了偏颇...

  本博客参考博客:

  Java协变和逆变:https://blog.csdn.net/qiuchengjia/article/details/52910901

  Java的逆变与协变:https://www.cnblogs.com/en-heng/p/5041124.html

  from Steven Shen

    编辑于2018.6.22

    修改于2019.9.4

  

Java中的协变与逆变的更多相关文章

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

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

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

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

  3. .net中的协变和逆变

    百度:委托中的协变和逆变. 百度:.net中的协变和逆变. 协变是从子类转为父类. 逆变是从父类到子类. 这样理解不一定严谨或者正确.需要具体看代码研究.

  4. C#4.0中的协变和逆变

    原文地址 谈谈.Net中的协变和逆变 关于协变和逆变要从面向对象继承说起.继承关系是指子类和父类之间的关系:子类从父类继承所以子类的实例也就是父类的实例.比如说Animal是父类,Dog是从Anima ...

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

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

  6. Java泛型的协变与逆变

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

  7. C#4.0新增功能03 泛型中的协变和逆变

    连载目录    [已更新最新开发文章,点击查看详细] 协变和逆变都是术语,前者指能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型,后者指能够使用比原始指定的派生类型的派生程度更小(不太具体 ...

  8. C# 中的协变和逆变

    作为一个从接触 Unity 3D 才开始学习 C# 的人,我一直只了解一些最基本.最简单的语言特性.最近看了<C# in Depth>这本书,发现这里面东西还真不少,即使除去和 Windo ...

  9. .NET泛型中的协变与逆变

    泛型的可变性:协变性和逆变性 实质上,可变性是以一种类型安全的方式,将一个对象作为另一个对象来使用. 我们已经习惯了普通继承中的可变性:例如,若某方法声明返回类型为Stream,在实现时可以返回一个M ...

随机推荐

  1. Leader:这样的 Bug 你也写的出来???

    Hello~各位读者新年好!不知道大家春节假期是否已延长,小黑哥刚接到通知,假期延长到 2 月 2 号,另外回去之后需要在家办公,自行隔离两周.还没试过在家办公,小黑哥就怕到时候生物钟还没调整过来,一 ...

  2. Python:元组类型

    概念 有序的 不可变的 元素集合 和列表的区别就是,元组是不可以修改的 定义 空元组:() 一个元素的元组: (a,),只有一个元素,要加一个逗号进行区分 多个元素的元组:(a, b, c) 除空元组 ...

  3. pytest-conftest.py作用范围

    1.conftest.py解释 conftest.py是pytest框架里面一个很重要的东西,它可以在这个文件里面编写fixture,而这个fixture的作用就相当于我们unittest框架里面的s ...

  4. 使用python爬取天气预报,[python入门案例]

    # 天气网余姚地区爬虫案例 import requests from lxml import etree class WeatherSpider: def __init__(self): self.u ...

  5. JS bind()方法、JS原生实现bind()

    一.arguments的含义 // arguments 是一个对应于传递给函数的参数的类数组对象 function a(){ console.log(arguments); } a(); // Arg ...

  6. Cisco TrustSec(理解)

    1.Cisco TrustSec的限制当指定了无效的设备ID时,受保护的访问凭据(Protected access credential,PAC)设置将失败并保持挂起状态. 即使在清除PAC并配置正确 ...

  7. lc 0219

    目录 ✅ 463. 岛屿的周长 描述 解答 cpp py ✅ 1122. 数组的相对排序 描述 解答 cpp py ✅ 876. 链表的中间结点 描述 解答 cpp ✅ 1160. 拼写单词 描述 解 ...

  8. String方法阅读笔记

    String类常用方法 1.int Length(): 参数:无 返回值:调用此方法的字符串的长度(int) 实例: public class Test { public static void ma ...

  9. spark-调节executor堆外内存

    什么时候需要调节Executor的堆外内存大小? 当出现一下异常时: shuffle file cannot find,executor lost.task lost,out of memory 出现 ...

  10. scrapy 和 scrapy-redis

    1.scrapy 是一个 Python 爬虫框架,爬取效率极高,但是不支持分布式.而 scrapy-redis 时一套基于 redis 数据库.运行在 scrapy 框架之上的组件,可以让 scrap ...