去年写过一篇博客谈了下我自己对协变和逆变的理解,现在回头看发现当时还是太过“肤浅”,根本没理解。不久前还写过一篇“黑”Java泛型的博客,猛一回头又是“肤浅”,今天学习Java泛型的时候又看到了协变和逆变,感觉又理解了点,记录一下,但不免还是“肤浅”,看了这篇博客的同学,欢迎留言交流下。Markdown

什么是协变和逆变?

到底什么是协变和逆变?先看例子:

//Java
Object[] objects = new String[2];
//C#
object[] objects = new string[2];

这就是协变,C#和Java都是支持数组协变的语言,好像说了等于没说,别急,慢慢来。

我们都知道C#和Java中String类型都是继承自Object的,姑且记做String ≦ Object,表示String是Object的子类型,String的对象可以赋给Object的对象。

而Object的数组类型Object[],我们可以理解成是由Object构造出来的一种新的类型,可以认为是一种构造类型,记f(Object)(可以类比下初中数学中函数的定义),那么我们可以这么来描述协变和逆变:

  • 当A ≦ B时,如果有f(A) ≦ f(B),那么f叫做协变
  • 当A ≦ B时,如果有f(B) ≦ f(A),那么f叫做逆变
  • 如果上面两种关系都不成立则叫做不可变

其实顾名思义,协变和逆变表示的一种类型转变的关系:“构造类型”之间相对“子类型”之间的一种关系。只不过平时我(可能也包括大家)被网上的一些文章搞糊涂了。“协”表示一种自然而然的转换关系,比如上面的String[] ≦ Object[],这就是大家学习面向对象编程语言中经常说的:

子类变量能赋给父类变量,父类变量不能赋值给子类变量。

而“逆”则不那么直观,平时用的也很少,后面讲Java泛型中的协变和逆变会看到例子。

不可变的例子就很多了,比如Java中List<Object>List<String>之间就是不可变的。

List<String> list1 = new ArrayList<String>();
List<Object> list2 = list1;

这两行代码在Java中肯定是编译不过的,反过来更不可能,C#中也是一样。

那么协变逆变作用到底是什么呢?我个人肤浅的理解:主要是语言设计的一种考量,目的是为了增加语言的灵活性和能力。

里氏替换原则

再说下面内容之前,提下这个大家都知道的原则:

有使用父类型对象的地方都可以换成子类型对象。

假设有类Fruit和Apple,Apple ≦ Fruit,Fruit类有一个方法fun1,返回一个Object对象:

public Object fun1() {
return null;
}
Fruit f = new Fruit();
//...
//某地方用到了f对象
Object obj = f.fun1();

那么现在Aplle对象覆盖fun1,假设可以返回一个String对象:

@Override
public String fun1() {
return "";
}
Fruit f = new Apple();
//...
//某地方用到了f对象
Object obj = f.fun1();

那么任何使用Fruit对象的地方都能替换成Apple对象吗?显然是可以的。

举得例子是返回值,如果是方法参数呢?调用父类方法fun2(String)的地方肯定可以被一个能够接受更宽类型的方法替代:fun2(Object)......

返回值协变和参数逆变

上面提到的Java和C#语言都没有把函数作为一等公民,那么那些支持一等函数的语言,即把函数也看做一种类型是如何支持协变和逆变的以及里氏原则的呢?

也就是什么时候用一个函数g能够替代其他使用函数f的地方。答案是:

函数f可以安全替换函数g,如果与函数g相比,函数f接受更一般的参数类型,返回更特化的结果类型。《维基百科》

这就是是所谓的对输入类型是逆变的而对输出类型是协变的Luca Cardelli提出的规则

虽然Java是面向对象的语言,但某种程度上它仍然遵守这个规则,见上一节的例子,这叫做返回值协变,Java子类覆盖父类方法的时候能够返回一个“更窄”的子类型,所以说Java是一门可以支持返回值协变的语言。

类似参数逆变是指子类覆盖父类方法时接受一个“更宽”的父类型。在Java和C#中这都被当作了方法重载

可能到这又绕糊涂了,返回值协变参数逆变又是什么东东?回头看看协变和逆变的理解。把方法当成一等公民: 
构造类型:Apple ≦ Fruit 
返回值:String ≦ Object 
参数:Object ≧ String

以上都是我个人对协变和逆变这两个概念的理解(欢迎拍砖)。说个题外话:“概念”是个很抽象的东西,之前听到一个不错说法,说概念这个单词英文叫做conceptcon表示“共同的”,cept表示“大脑”。

Java泛型中的协变和逆变

一般我们看Java泛型好像是不支持协变或逆变的,比如前面提到的List<Object>List<String>之间是不可变的。但当我们在Java泛型中引入通配符这个概念的时候,Java 其实是支持协变和逆变的。

看下面几行代码:

// 不可变
List<Fruit> fruits = new ArrayList<Apple>();// 编译不通过
// 协变
List<? extends Fruit> wildcardFruits = new ArrayList<Apple>();
// 协变->方法的返回值,对返回类型是协变的:Fruit->Apple
Fruit fruit = wildcardFruits.get(0);
// 不可变
List<Apple> apples = new ArrayList<Fruit>();// 编译不通过
// 逆变
List<? super Apple> wildcardApples = new ArrayList<Fruit>();
// 逆变->方法的参数,对输入类型是逆变的:Apple->Fruit
wildcardApples.add(new Apple());

可见在Java泛型中通过extends关键字可以提供协变的泛型类型转换,通过supper可以提供逆变的泛型类型转换。

关于Java泛型中supperextends关键字的作用网上有很多文章,这里不再赘述。只举一个《Java Core》里面supper使用的例子:下面的代码能够对实现Comparable接口的对象数组求最小值。

public static <T extends Comparable<T>> T min(T[] a) {
if (a == null || a.length == 0) {
return null;
}
T t = a[0];
for (int i = 1; i < a.length; i++) {
if (t.compareTo(a[i]) > 0) {
t = a[i];
}
}
return t;
}

这段代码对Calendar类是运行正常的,但对GregorianCalendar类则无法编译通过:

Calendar[] calendars = new Calendar[2];
Calendar ret3 = CovariantAndContravariant.<Calendar> min(calendars);
GregorianCalendar[] calendars2 = new GregorianCalendar[2];
GregorianCalendar ret2 = CovariantAndContravariant.<GregorianCalendar> min(calendars2);//编译不通过

如果想工作正常需要将方法签名修改为:

public static <T extends Comparable<? super T>> T min(T[] a)

至于原因,大家看下源码和网上大量关于supper的作用应该就明白了,我这里希望能够给看了上面内容的同学提供另外一个思路......

C#虽然不支持泛型类型的协变和逆变(接口和委托是支持的,我之前的那篇博客也提到了),至于为什么C#不支持,《深入解析C#》中说是主要归结于两种语言泛型的实现不同:C#是运行时的,Java只是一个“编译时”特性。但究竟是为什么还是没说明白,希望有时间再研究下。

参考资料

维基百科

再谈对协变和逆变的理解(Updated)的更多相关文章

  1. 在net中json序列化与反序列化 面向对象六大原则 (第一篇) 一步一步带你了解linq to Object 10分钟浅谈泛型协变与逆变

    在net中json序列化与反序列化   准备好饮料,我们一起来玩玩JSON,什么是Json:一种数据表示形式,JSON:JavaScript Object Notation对象表示法 Json语法规则 ...

  2. c# 协变和逆变的理解

    1. 是什么 1.1 协变 协变指能够使用比原始指定的派生类型的派生程度更小(不太具体的)的类型.如 string 到 object 的转换.多见于类型参数用作方法的返回值. 1.2 逆变 逆变指能够 ...

  3. 深入理解 C# 协变和逆变

    msdn 解释如下: “协变”是指能够使用与原始指定的派生类型相比,派生程度更大的类型. “逆变”则是指能够使用派生程度更小的类型. 解释的很正确,大致就是这样,不过不够直白. 直白的理解: “协变” ...

  4. 【转】深入理解 C# 协变和逆变

    http://www.cnblogs.com/qixuejia/p/4383068.html 深入理解 C# 协变和逆变   msdn 解释如下: “协变”是指能够使用与原始指定的派生类型相比,派生程 ...

  5. 深入理解 C# 协变和逆变 (转载)

      深入理解 C# 协变和逆变 msdn 解释如下: “协变”是指能够使用与原始指定的派生类型相比,派生程度更大的类型. “逆变”则是指能够使用派生程度更小的类型. 解释的很正确,大致就是这样,不过不 ...

  6. [C#]浅谈协变与逆变

    看过几篇说协变与逆变的博客,虽然都是正确无误的,但是感觉都没有说得清晰明了,没有切中要害.那么我也试着从我的理解角度来谈一谈协变与逆变吧. 什么是协变与逆变 MSDN的解释:https://msdn. ...

  7. C# 泛型的协变和逆变

    1. 可变性的类型:协变性和逆变性 可变性是以一种类型安全的方式,将一个对象当做另一个对象来使用.如果不能将一个类型替换为另一个类型,那么这个类型就称之为:不变量.协变和逆变是两个相互对立的概念: 如 ...

  8. .NET 4.0中的泛型的协变和逆变

    转自:http://www.cnblogs.com/jingzhongliumei/archive/2012/07/02/2573149.html 先做点准备工作,定义两个类:Animal类和其子类D ...

  9. 转载.NET 4.0中的泛型的协变和逆变

    先做点准备工作,定义两个类:Animal类和其子类Dog类,一个泛型接口IMyInterface<T>, 他们的定义如下:   public class Animal { } public ...

随机推荐

  1. i春秋——春秋争霸write up

    i春秋--春秋争霸write up 第一关 题目给出一张图 提示中,这种排列源于古老的奇书,暗含了两个数字,可以得出第一关的答案是两个数字 百度识图来一发, 得到图中排列是来自于洛书,点开洛书的百度百 ...

  2. .NET MVC框架中控制器接收参数的四种方式

    1.通过路由中的配置的参数名字直接接收(要求:两者同名) routes.MapRoute(                 name: "Default",             ...

  3. 【JS笔记】私有变量

    1.任何函数中定义的变量都可以认为是私有变量.函数内部可以访问,外部不能访问. 可以通过闭包创建特权方法访问私有变量. function Foo(){ var n=10; this.returnN=f ...

  4. 第一章 OO大智慧

    今天,正式开始读王涛写的<你必须知道的.NET(第二版)>,刚开始读了序,觉得写的相当精彩,就被吸引住了.看了一会发现本书的特点可能就是以例举例,形象生动,比较期待的样子.虽然前面讲的概念 ...

  5. Git.Framework 框架随手记--ORM查询数据集合 一

    本文记录Git.Framework之ORM中最为浓墨重彩的一篇,查询集合.根据自己做的项目统计这个是使用频率最高的一个. 一. 查询集合方法简介 (1)List<T> GetList(); ...

  6. 每天一个linux命令(42):crontab命令

    前 一天学习了 at 命令是针对仅运行一次的任务,循环运行的例行性计划任务,linux系统则是由 cron (crond) 这个系统服务来控制的. Linux 系统上面原本就有非常多的计划性工作,因此 ...

  7. eclipse快捷键的使用及概述

    <eclipse快捷键的使用及概述> <Eclipse概述>       Eclipse 是一个开放源代码的.基于Java的可扩展开发平台.就其本身而言,它只是一个框架和一组服 ...

  8. 从零开始设计SOA框架(三):请求参数的加密方式

    第二章中说明请求参数有哪些,主要是公共参数和业务参数,服务端需要对参数进行效验,已验证请求参数的合法性 参数效验前先解释下以下参数: 1.参数键值对:包括公共参数.业务参数      1.公共参数:按 ...

  9. Html-Css-a标签的使用

    a标签去掉下划线 a{ text-decoration:none; } 或者把这个属性分别加到a标签下, a:link{ text-decoration:none; } a:visited{ text ...

  10. 判断Set里的元素是否重复、==、equals、hashCode方法研究-代码演示

    被测试类,没有重写hasCode()和equals()方法: package niukewang; import java.util.Objects; public class setClass { ...