协变和逆变

onlyloveyd
• 阅读 1578

本文同步发表于我的微信公众号,在微信搜索 OpenCV or Android 即可关注。

协变、逆变

概念

许多程序设计语言的类型系统支持子类型。例如,如果Cat是Animal的子类型,那么Cat类型的表达式可用于任何出现Animal类型表达式的地方。所谓的变型(variance)是指如何根据组成类型之间的子类型关系,来确定更复杂的类型之间(例如Cat列表之于Animal列表,回传Cat的函数之于回传Animal的函数...等等)的子类型关系。当我们用类型构造出更复杂的类型,原本类型的子类型性质可能被保持、反转、或忽略───取决于类型构造器的变型性质

协变与逆变用来描述类型转换(type transformation)后的继承关系:A、B表示类型,f表示类型转换,A≦ B表示A为B的子类,那么则存在:

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

具象化

定义Cat、Animal两个类型,且Cat是Animal的子类,类型构造器采用数组的形式:

  • 协变(covariant):一个Cat[]也是一个Animal[]
  • 逆变(contravariant):一个Animal[]也是一个Cat[]
  • 不变(invariant):以上两种均不满足。

总结:假想程序设计语言中的类型为输入,数组、列表、泛型等类型构造器为函数,当函数值与输入正相关时为协变,当函数值与输入负相关时为逆变。父子类关系代表输入的大小。

语言场景

Java

Java语言中,数组支持协变,泛型类原生既不支持协变,也不支持逆变。

定义类Dog,类Animal,且Dog为Animal的子类

public static void main(String[] args) {
    Dog[] dogs = new Dog[5];
    Animal[] animals = dogs; // Java数组支持协变

    ArrayList<Dog> dogList = new ArrayList<>();
    ArrayList<Animal> animalList = dogList; // 编译报错,Java泛型直接使用不支持协变
}

显然,Dog ≦ Animal,Dog[] ≦ Animal[],Java数组支持协变,不支持逆变,也就是说子类数组对象可以赋值给父类数组申明,但是父类数组对象不能赋值给子类数组申明。而针对Java泛型,直接使用既不支持协变也不支持逆变。逆变不支持好理解,为啥Java泛型连协变也不支持呢?

因为类型擦除。泛型虽然是 Java 1.5 版本引进的概念,但是,泛型代码能够很好地和旧版本代码兼容。因为Java为了兼容老版本,将与泛型相关的类型信息进行了擦除。关于类型擦除,有一道经典面试题:

public static void typeEraseSample() {
    ArrayList<Integer> intList = new ArrayList<>();
    ArrayList<String> strList = new ArrayList<>();
    boolean isSameClass = intList.getClass() == strList.getClass()
    System.out.printf(String.valueOf(isSameClass));
}

如上代码最后输入为何?终端输出:true。何解?查看字节码,一目了然。

协变和逆变

利用类型擦除,我们可以采用反射方式向Java列表对象中添加任何类型的对象。

public static void hackTypeErase() {
    ArrayList<Integer> intList = new ArrayList<>();
    intList.add(23);
    try {
        Method method = intList.getClass().getDeclaredMethod("add", Object.class);
        method.invoke(intList, new Dog());
        method.invoke(intList, "yidong");
    } catch (Exception e) {
        e.printStackTrace();
    }
    for (Object object : intList) {
        System.out.println(object);
    }
}

协变和逆变

举例只是为了说明问题,并不是推荐大家这样操作。继续回到协变和逆变,Java泛型直接使用不支持协变和逆变,但是通过Java提供的泛型通配符,我们可以做到返回值协变和参数逆变。

泛型通配符

PECS原则:Producer extends,Consumer super。

上界通配符:? extends T

// 泛型协变
public static void covariantGeneric() {
    List<? extends Animal> objList = new ArrayList<Dog>() {{
        add(new Dog());
        add(new Dog());
    }};
    Animal animal = objList.get(0);//编译通过
    Dog dog = objList.get(0);      //编译报错
    objList.add(new Animal());     //编译报错
    objList.add(new Dog());        //编译报错
}

通俗理解:? extends Animal,代表的是Animal及其子类,所以Animal是类型上界,由此来理解“上界通配符”这个名称。针对返回值是泛型的方法(示例中get方法),由于子类对象可以赋值给父类引用,所以必须用Animal或者其父类引用来接收,体现协变的转型一致性。针对参数是泛型的方法(示例中add方法),为了保持确定性,不允许执行该类操作,因为无法确定程序传入的子类对象类型,倘若允许此类操作,在获取列表元素时,就会存在明显的类型不安全。

总结:? extends T表示所存储类型都是T及其子类,但是获取元素所使用的引用类型只能是T或者其父类。使用上限通配符实现向上转型,但是会失去存储对象的能力,上限通配符为集合的协变表示。适用于只使用,不修改的场景,也就是生产者角色。

下界通配符:? super T

// 泛型逆变
public static void contravariantGeneric() {
    List<? super Dog> objList = new ArrayList<Animal>() {{
        add(new Animal());
        add(new Animal());
    }};
    Animal animal = objList.get(0);//编译报错
    Dog dog = objList.get(0);      //编译报错
    Object object = objList.get(0);//编译正常
    objList.add(new Animal());     //编译报错
    objList.add(new Dog());        //编译通过
}

通俗理解:? super Dog,代表的是Dog及其父类,所以Dog是类型下界,由此来理解“下界通配符”这个名称。针对返回值是泛型的方法,由于子类对象只能赋值给自己或者父类引用,但是我们并不能保证返回的对象的继承关系比引用类型低,所以除了用Object引用接受,其他的类型接受均是不被允许的。而针对参数是泛型的方法(示例中add方法),由于Dog是继承关系的最底层,所以传入Dog或者其子类对象,列表元素引用是必然可以接收的,所以该操作是被允许的。

总结:下限通配符 ? super T表示 所存储类型为T及其父类,但是添加的元素类型只能为T及其子类,而获取元素所使用的类型只能是Object,因为Object为所有类的父类。下限通配符为集合的逆变表示。适用于只修改,不使用的场景,也就是消费者角色。

无界通配符:?

只使用类型无关的方法时可采用无界通配符。

public static int getLength(List<?> list) {
    return list.size();
}

Kotlin

Kotlin泛型直接使用不支持协变,也不支持逆变。由于Kotlin数组也是采用泛型的形式实现的,所以也不支持协变和逆变。与Java语言对应,Kotlin也可以使用关键字in和out来打开协变和逆变的限制,但是使用过程中同样存在和Java一样的限制。

  • in关键字对应? super
  • out关键字对应? extends

Kotlin和Java关键字名称很好的诠释了PECS法则:Producer exends, Consumer super

为了方便记忆,可以合并一下:Producer (out) exends, Consumer (in) super

完整示例如下:

// 类型擦除
fun typeEraseSample() {
    val intList = ArrayList<Int>()
    val strList = ArrayList<String>()
    val isSameClass = intList.javaClass == strList.javaClass
    System.out.printf(isSameClass.toString())
}

// 利用反射完成填充操作
fun hackTypeErase() {
    val intList = ArrayList<Int>()
    intList.add(23)
    try {
        val method = intList.javaClass.getDeclaredMethod("add", Any::class.java)
        method.invoke(intList, Dog())
        method.invoke(intList, "yidong")
    } catch (e: Exception) {
        e.printStackTrace()
    }
    for (obj in intList) {
        println(obj)
    }
}

// 泛型协变
fun covariantGeneric() {
    val objList: MutableList<out Animal> = MutableList(5) { Dog() }
    val animal = objList[0] //编译通过
    val dog: Dog = objList[0] //编译报错
    objList.add(Animal()) //编译报错
    objList.add(Dog()) //编译报错
}

// 泛型逆变
fun contravariantGeneric() {
    val objList: MutableList<in Dog> = MutableList(5) { Animal() }
    val animal: Animal = objList[0] // 编译报错
    val dog: Dog = objList[0] // 编译报错
    val obj: Any? = objList[0] // 编译正常
    objList.add(Animal()) //编译报错
    objList.add(Dog()) //编译通过
}

// 获取列表长度
fun getLength(list: List<*>): Int {
    return list.size
}

最后介绍一个Java里没有的内容,具体化类型参数【reified】。在Java语言中,类型参数并不是一个真正的类型,而只是一个代号,我们无法把它当成一个普通类型使用,比如无法调用instanceof函数。但是在Kotlin中,我们可以通过reified关键字来具体化类型参数,但是只能在内联方法中使用,因为 Kotlin 编译器会把内联函数的代码插入到调用者的地方,所以可以在编译期就确定泛型的类型。

下面这个简单的方法很好的体现了reified的作用:

inline fun <reified R> isInstanceOf(t: Any) = t is R

总结

为了方便记忆,首尾呼应一下:

协变和逆变

协变和逆变

参考链接:

https://baike.baidu.com/item/%E5%8D%8F%E5%8F%98/10963814?fr=aladdin https://blog.csdn.net/zy_jibai/article/details/90082239 https://www.zybuluo.com/zhanjindong/note/34147 https://www.bilibili.com/video/BV1T441117u8

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Wesley13 Wesley13
3年前
java 数组的协变和逆变
先说结论:1.基元类型数组不允许协变和逆变,无法编译通过。2.引用类型数组允许协变和逆变,逆变时会检查实际类型,如果不相符则抛出java.lang.ClassCastException。下面是验证代码。1publicclassTestArrayInstance{2publicst
Stella981 Stella981
3年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Stella981 Stella981
3年前
Android蓝牙连接汽车OBD设备
//设备连接public class BluetoothConnect implements Runnable {    private static final UUID CONNECT_UUID  UUID.fromString("0000110100001000800000805F9B34FB");
Wesley13 Wesley13
3年前
ThinkPHP 根据关联数据查询 hasWhere 的使用实例
很多时候,模型关联后需要根据关联的模型做查询。场景:广告表(ad),广告类型表(ad\_type),现在需要筛选出广告类型表中id字段为1且广告表中status为1的列表先看关联的设置部分 publicfunctionadType(){return$thisbelongsTo('A
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这