协变和逆变

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

  • 6
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Java泛型中的协变逆变都是针对类型转换的规定。 协变(covariant):指的是继承链中子类(派生类)类型能够作为父类(基类)类型的一种属性,也就是子类可以作为父类使用的能力。在泛型中,协变的概念可以用来表示如果类型A是类型B的一个子类型,那么泛型类G<A>就可以视作泛型类G<B>的一个子类型。 例子: ```java // Animal类 public class Animal {} // Dog类是Animal类的子类 public class Dog extends Animal {} // 泛型接口List public interface List<E> { void add(E e); E get(int index); } // 定义一个方法acceptList,其形参类型为List<? extends Animal> public static void acceptList(List<? extends Animal> list) { for (Animal animal : list) { // ... } } // List类型为List<Dog> List<Dog> list = new ArrayList<Dog>(); list.add(new Dog()); acceptList(list); // 在这里,我们可以传入一个List<Dog>参数,因为Dog类是Animal类的子类 ``` 逆变(contravariant):指的是继承链中父类(基类)类型能够作为子类(派生类)类型的一种属性,也就是父类可以作为子类使用的能力。在泛型中,逆变的概念可以用来表示如果类型A是类型B的一个超类型,那么泛型类G<B>就可以视作泛型类G<A>的一个子类型。 例子: ```java // Animal类 public class Animal {} // Dog类是Animal类的子类 public class Dog extends Animal {} // 泛型接口Comparator public interface Comparator<T> { int compare(T o1, T o2); } // 定义一个方法sortList,其形参类型为List<? super Dog> public static void sortList(List<? super Dog> list) { // ... } // List类型为List<Animal> List<Animal> list = new ArrayList<Animal>(); list.add(new Animal()); sortList(list); // 在这里,我们可以传入一个List<Animal>参数,因为Animal类是Dog类的超类型 ``` extends和super关键字常常用于定义泛型类型参数的上边界(upper bound)和下边界(lower bound)。extends表示类型参数的上限,超过这个范围就会导致编译错误;super表示类型参数的下限,超过这个范围也会导致编译错误。 例子: ```java // 泛型类Pair,其类型参数T有上限(用extends)为Comparable<? super T>,表示类型T要么是Comparable<? super T>本身,要么是Comparable<? super T>的子类型 public class Pair<T extends Comparable<? super T>> { private T first; private T second; public Pair(T first, T second) { this.first = first; this.second = second; } public T getFirst() { return first; } public T getSecond() { return second; } public T max() { return first.compareTo(second) >= 0 ? first : second; } } // Pair类型为Pair<String> Pair<String> pair = new Pair<String>("hello", "world"); String max = pair.max(); // 在这里,我们可以调用max方法,因为String类实现了Comparable<String>接口 ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AndroidKt

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值