协变和逆变

onlyloveyd 等级 744 0 0

本文同步发表于我的微信公众号,在微信搜索 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

收藏
评论区

相关推荐

Swift 简介
Swift和ObjectiveC的主要区别 1,编程范式 Swift可以面向协议编程、函数式编程、面向对象编程。 Swift语言引入了协议、协议的扩展、泛型等新特性,因此使用Swift语言可以很好地面向协议编程;Swift语言将函数和闭包提升为语言的一等公民,函数可以作为一个变量、可以作为其他函数的参数、作为其他函数的返回值等来传递,所以
协变和逆变
本文同步发表于我的微信公众号,在微信搜索 OpenCV or Android 即可关注。 协变、逆变 概念 许多程序设计语言的类型系统支持子类型。例如,如果Cat是Animal的子类型,那么Cat类型的表达式可用于任何出现Animal类型表达式的地方。所谓的变型(variance)是指如何根据组成类型之间的子类型关系,来确定更复杂的类型之间(例如C
TS 的脚步已经拦不住,代码撸起来
前言 vue3已经发布了,ts的脚步已经阻拦不住了,还只会es6?别想了,人家都已经在行动了,以下是ts的基本系列教程,ts的基本语法,高级语法等,以及在vue项目中如何应用ts,跟着我赶紧撸起来吧。 基本数据类型 数字 const a: number  3; 字符串 const b: string  "1
Dart中的泛型、泛型方法、泛型类、泛型接口
一、Dart中的泛型 泛型方法 通俗理解:泛型就是解决 类 接口 方法的复用性、以及对不特定数据类型的支持(类型校验) 一般用   T   表示泛型 getData<T(T value){ return
JDK8在泛型类型推导上的变化
> 本文来自: [PerfMa技术社区](https://www.oschina.net/action/GoToLink?url=https%3A%2F%2Fclub.perfma.com) > > [PerfMa(笨马网络)官网](https://www.oschina.net/action/GoToLink?url=https%3A%2F%2Fwww.
Java变量类型
在Java语言中,所有的变量在使用前必须声明。声明变量的基本格式如下: type identifier [ = value][, identifier [= value] ...] ; 格式说明:type为Java数据类型。identifier是变量名。可以使用逗号隔开来声明多个同类型变量。 Java语言支持的变量类型有: * 类变量:独
java 实现websocket
最近了解了下websocket和socket这个东西,说不得不来说下为何要使用 WebSocket ,和为何不用http。 **为何需要WebSocket ?** HTTP 协议是一种无状态的、无连接的、单向的应用层协议。它采用了请求/响应模型。通信请求只能由客户端发起,服务端对请求做出应答处理。 这种通信模型有一个弊端:HTTP 协议无法实现服务器主
java 数组的协变和逆变
先说结论: 1. **基元类型数组不允许协变和逆变,无法编译通过。** 2. **引用类型数组允许协变和逆变,逆变时会检查实际类型,如果不相符则抛出java.lang.ClassCastException。** 下面是验证代码。 1 public class TestArrayInstance { 2 public st
java 泛型详解
对java的泛型特性的了解仅限于表面的浅浅一层,直到在学习设计模式时发现有不了解的用法,才想起详细的记录一下。 本文参考java 泛型详解、Java中的泛型方法、 java泛型详解 1\. 概述 泛型在java中有很重要的地位,在面向对象编程及各种设计模式中有非常广泛的应用。 什么是泛型?为什么要使用泛型? 泛型,即“参数化类型”。一提到参数,最熟
java几个类的简单使用
##Random Random类用来创建一个新的随机数生成器。 * * * ##对象数组 ArrayList集合的长度是可以随意改变的。 ArrayList<E> 这个<E>代表泛型 泛型:装在集合当中的所有元素,全部都是统一的类型。泛型只能是引用类型,不能使用基本元素。 import java.util.ArrayList;
java泛型总结
###1. 特点, 好处 java1.5后出现 包含1.5版本 泛型的出现 解决程序的安全性 保证程序的一致安全机制 使用泛型 避免了类型的强制类型转换 代码就简单 数据类型只能是 应用类型 **不能使基本类型,且前后保持一致** 泛型的 定义格式: > 集合类<数据类型>变量 = new集合类<数据类型>(); ###2. 定义使用
20175209 《Java程序设计》第八周学习总结
20175209 《Java程序设计》第八周学习总结 ========================== ### 一、教材知识点总结 #### 1.泛型 1.泛型类声明: * 格式 `class People<E>` * People是泛型类名称 * E是泛型列表,可以是任何对象或接口,但不能是基本类型数据
20175209 《Java程序设计》第八周学习总结
20175209 《Java程序设计》第八周学习总结 ========================== ### 一、教材知识点总结 #### 1.泛型 1.泛型类声明: * 格式 `class People<E>` * People是泛型类名称 * E是泛型列表,可以是任何对象或接口,但不能是基本类型数据
Gson通过借助TypeToken获取泛型参数的类型的方法
最近在使用Google的Gson包进行Json和Java对象之间的转化,对于包含泛型的类的序列化和反序列化Gson也提供了很好的支持,感觉有点意思,就花时间研究了一下。 由于Java泛型的实现机制,使用了泛型的代码在运行期间相关的泛型参数的类型会被擦除,我们无法在运行期间获知泛型参数的具体类型(所有的泛型类型在运行时都是Object类型)。 但是有的时候
PHPcpp 变量和类型
    用PHPCPP来开发PHP扩展是非常容易的,最主要的就是变量类型和PHP中的变量类型一毛一样,最重要的是写法也是一毛一样。     在PHP中,变量默认是没有类型的,我们赋给他整数,他就是整形,赋给他字符串他就是string,也就是说PHP中的变量类型是随着值来定义的。PHPCPP在这里也是做了很大的优化,实现了类型随值的类型来定义。 P