Java中的协变与逆变
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中的协变与逆变的更多相关文章
- Scala中的协变,逆变,上界,下界等
Scala中的协变,逆变,上界,下界等 目录 [−] Java中的协变和逆变 Scala的协变 Scala的逆变 下界lower bounds 上界upper bounds 综合协变,逆变,上界,下界 ...
- Java泛型中的协变和逆变
Java泛型中的协变和逆变 一般我们看Java泛型好像是不支持协变或逆变的,比如前面提到的List<Object>和List<String>之间是不可变的.但当我们在Java泛 ...
- .net中的协变和逆变
百度:委托中的协变和逆变. 百度:.net中的协变和逆变. 协变是从子类转为父类. 逆变是从父类到子类. 这样理解不一定严谨或者正确.需要具体看代码研究.
- C#4.0中的协变和逆变
原文地址 谈谈.Net中的协变和逆变 关于协变和逆变要从面向对象继承说起.继承关系是指子类和父类之间的关系:子类从父类继承所以子类的实例也就是父类的实例.比如说Animal是父类,Dog是从Anima ...
- Java语言中的协变和逆变(zz)
转载声明: 本文转载至:http://swiftlet.net/archives/1950 协变和逆变指的是宽类型和窄类型在某种情况下的替换或交换的特性.简单的说,协变就是用一个窄类型替代宽类型,而逆 ...
- Java泛型的协变与逆变
泛型擦除 Java的泛型本质上不是真正的泛型,而是利用了类型擦除(type erasure),比如下面的代码就会出现错误: 报的错误是:both methods have same erasure ...
- C#4.0新增功能03 泛型中的协变和逆变
连载目录 [已更新最新开发文章,点击查看详细] 协变和逆变都是术语,前者指能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型,后者指能够使用比原始指定的派生类型的派生程度更小(不太具体 ...
- C# 中的协变和逆变
作为一个从接触 Unity 3D 才开始学习 C# 的人,我一直只了解一些最基本.最简单的语言特性.最近看了<C# in Depth>这本书,发现这里面东西还真不少,即使除去和 Windo ...
- .NET泛型中的协变与逆变
泛型的可变性:协变性和逆变性 实质上,可变性是以一种类型安全的方式,将一个对象作为另一个对象来使用. 我们已经习惯了普通继承中的可变性:例如,若某方法声明返回类型为Stream,在实现时可以返回一个M ...
随机推荐
- CAS 和 ABA 问题
CAS简介 CAS 全称是 compare and swap,是一种用于在多线程环境下实现同步功能的机制. CAS 它是一条CPU并发原语.操作包含三个操作数 -- 内存位置.预期数值和新值.CAS ...
- SqlHelper类编写前奏:DataReader关闭链接出现问题
SqlHelper是一个执行数据库操作的助手类,但是当我们没学过DataSet之前,要想使用using搭配SqlConnection和SqlCommand写出一个真正独立的SqlHelper都是不太可 ...
- Python:时间日历基本处理
time 模块 提供了处理时间和表示之间转换的功能 获取当前时间戳 时间戳:从0时区的1970年1月1日0时0分0秒,到所给定日期时间的时间,浮点秒数,或者毫秒整数 获取方式: import time ...
- 模块学习-shutil
高级的 文件.文件夹.压缩包 处理模块 shutil.copyfileobj(fsrc, fdst[, length]) 将文件内容拷贝到另一个文件中,可以部分内容 shutil.copyfile(s ...
- 兔子与兔子(字符串hash)
传送门 很久很久以前,森林里住着一群兔子. 有一天,兔子们想要研究自己的 DNA 序列. 我们首先选取一个好长好长的 DNA 序列(小兔子是外星生物,DNA 序列可能包含 26 个小写英文字母). 然 ...
- ES5-bind用法及与以前的apply和call
当我们调用一个函数的时候,函数中的this一般是指向调用者的.但是我们其实可以在调用函数的时候,传入一个对象,让函数中的this指向我们传入的对象,而不是调用者本身. apply,call,bind都 ...
- java获取当前机器的公网ip
package com.Interface.util; import javax.servlet.http.HttpServletRequest; /** * 测试类 * * @author 华文 * ...
- 判断一个数组是否包含一个指定的值 includes-ES6
var array1 = [1, 2, 3]; console.log(array1.includes(2)); // trueconsole.log(array1.includes(2, 5)); ...
- 【原】centos安装django
一.更新系统软件包yum update -y 二.安装软件管理包和可能使用的依赖 yum -y groupinstall "Development tools" yum insta ...
- Codeforces Round #606 (Div. 2) - E. Two Fairs(割点+dfs)
题意:给你一张无向连通图,对于求有多少对$(x,y)$满足互相到达必须经过$(a,b)$,其中$x\neq a,x\neq b,y\neq a,y\neq b$ 思路:显然$a,b$都必须为割点,所以 ...