秒懂Kotlin之协变(Covariance)逆变(Contravariance)与抗变(Invariant)

[版权申明] 非商业目的注明出处可自由转载
博文地址:https://blog.csdn.net/ShuSheng0007/article/details/108708218
出自:shusheng007

文章首发于个人博客

概述

协变,逆变,抗变等概念是从数学中来的,在编程语言Java/Kotlin/C#中主要在泛型中使用。其描述的是两个类型集合之间的继承关系。有兴趣可以阅读这篇文章 An Illustrated Guide to Covariance and Contravariance in Kotlin。本文应该属于进阶知识,一般小白程序员不是没听说过就是听说过但是完全搞不明白其中的奥妙。看到即赚到,这又将是你进阶的一个台阶。

定义

首先让我们搞明白这三个名词的概念吧:

假设我们有如下两个类型集合

第一个集合为: AnimalDog , DogAnimal的子类

open class Animal 
class Dog : Animal()

第二个集合为 List<Animal> List<Dog>

List<Animal>
List<Dog>

现在问题来了:由于DogAnimal的子类,那么List<Dog>就是List<Animal>的子类这句话在Kotlin/Java中对吗?

相信有一定Java/Kotlin编程经验的都可以回答的出来,答案是否定的。我们这里要说的协变,逆变,抗变就是描述上面两个类型集合的关系的。

  • 协变(Covariance):List<Dog>List<Animal>的子类型

  • 逆变(Contravariance): List<Animal>List<Dog>的子类型

  • 抗变(Invariant): List<Animal>List<Dog>没有任何继承关系

A subtype must accept at least the same range of types as its supertype declares.
A subtype must return at most the same range of types as its supertype declares.

Java中的情形

由于Kotlin是尝试对Java的改进,所以我们先来看Java的情况:

抗变

Java中泛型是抗变的,那就意味着List<String>不是List<Object>的子类型。因为如果不这样的话就会产生类型不安全问题。

例如下面代码可以通过编译的话,就会在运行时抛出异常

List<String> strs = new ArrayList<String>();
List<Object> objs = strs; 
objs.add(1); 
 // 尝试将Integer 转换为String,发生运行时异常 ClassCastException: Cannot cast Integer to String
String s = strs.get(0);

所以上面的代码在编译时就会报错,这就保证了类型安全。

但值得注意的是Java中的数组是协变的,所以数组真的会遇到上面的问题,编译可以正常通过,但会发生运行时异常,所以在Java中要优先使用泛型集合。

 String[] strs= new String[]{"ss007"};
 Object[] objs= strs;
 objs[0] = 1;

协变

抗变性会严重制约程序的灵活性,例如有如下方法copyAll,将一个String集合的内容copy到一个Object集合中,这是顺理成章的事。

// Java
void copyAll(Collection<Object> to, Collection<String> from) {
     to.addAll(from);
}

但是如果Collection<E>中的addAll方法签名如下的话,copyAll方法就通不过编译,因为通过上面的讲解,我们知道由于抗变性,Collection<String>不是Collection<Object>的子类,所以编译通不过。

boolean addAll(Collection<E> c);

那怎么办呢?

Java通过通配符参数(wildcard type argument)来解决, 把addAll的签名改成如下即可:

boolean addAll(Collection<? extends E> c);

? extends E 表示此方法可以接收E或者E的子类的集合。此通配符使得泛型类型协变了。

逆变

同理有时我们需要将Collection<Object>传递给Collection<String>就使用? super E,其 表示可以接收E或者E的父类,子类的位置却可以接收父类的实例,这就使得泛型类型发生了逆变

void m (List<? super String){
}

协变与逆变的特性

当使用? extends E 时,只能调用传入参数的读取方法而无法调用其修改方法。
当使用? super E时,可以调用输入参数的修改方法,但调用读取方法的话返回值类型永远是Object,几乎没有用处。

是不是感觉不好理解,确实不好理解!让我们一起看下code吧,理解了Java的这块,Kotlin的Inout关键字就手到擒来了。

例如有如下一接口,其有两个方法,一个修改,一个读取。

interface BoxJ<T> {
      T getAnimal();
      void putAnimal(T a);
  }

下面是两个使用通配符的方法,注意看注释

//协变,可以接受BoxJ<Dog>类型的参数
 private Animal getOutAnimalFromBox(BoxJ<? extends Animal> box) {
       Animal animal = box.getAnimal();
      // box.putAnimal(某个类型) 无法调用该修改方法,因为无法确定 ?究竟是一个什么类型,没办法传入
       return animal;
  }

//逆变,可以接受BoxJ<Animal>类型的参数
 private void putAnimalInBox(BoxJ<? super Dog> box){
        box.putAnimal(new Dog());
        // 虽然可以调用读取方法,但返回的类型却是Object,因为我们只能确定 ?的最顶层基类是Object
        Object animal= box.getAnimal();
  }

关于Java的通配符如何使用, Effective Java, 3rd Edition 的作者将其总结为:PECS : stands for Producer-Extends, Consumer-Super. 结合上面代码分析是不是觉得很精辟。

  • Producer-Extends 只能调用读取方法,向外提供数据,无法调用修改方法
  • Consumer-Super 一般只调用修改方法,消费从外面获取的数据,调用读取方法几乎没什么用,拿到的类型永远是Object

建议自己动手尝试一下,不然还是会有点懵

那Java这种方式有没有弊端呢?Kotlin官方认为有,但是我却没怎么领会,请原谅我。其大概的意思就是说:增加了复杂性,但却没有获得相应的好处。

Kotlin中的情形

请移步到下篇… 秒懂Kotlin之彻底弄懂形变注解out与in

总结

协变,逆变和抗变,听听,你听听,是不是感觉是特别高深的概念啊,我第一次接触还是看英文文档的时候,那叫一个懵逼啊,现在看来不过如此。又应了那句老话:难者不会,会者不难。

最后记得点赞,分享加收藏

孤村落日残霞,轻烟老树寒鸦。一点飞鸿影下,青山绿水,白草红叶黄花。《 天净沙 秋》

  • 19
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 12
    评论
Kotlin中的泛型是一种类型变量的机制,它允许我们在不确定具体类型的情况下编写通用代码。Kotlin中的泛型支持协变逆变,以及in和out的关键字。 首先,我们来看一个简单的例子,实现一个泛型容器类: ```kotlin class Container<T>(var item: T) { fun getItem(): T { return item } } fun main() { val container = Container<String>("Hello") println(container.getItem()) } ``` 在这个例子中,我们定义了一个名为Container的泛型类,它有一个类型参数T。我们可以创建一个Container实例,并将其实例化为一个具体类型。我们使用getItem方法来获取这个容器中的item。 接下来,我们来介绍一下协变逆变。假设我们有两个类: ```kotlin open class Animal { fun makeSound() { println("Making animal sound") } } class Cat: Animal() { fun meow() { println("Meow") } } ``` 我们可以通过一个简单的示例来说明协变逆变: ```kotlin fun main() { val animals: List<Animal> = listOf(Cat(), Animal()) makeSounds(animals) } fun makeSounds(animals: List<Animal>) { for (animal in animals) { animal.makeSound() } } ``` 在这个例子中,我们定义了一个List<Animal>类型的变量animals,它包含了一个Cat和一个Animal实例。我们将这个变量传递给了makeSounds函数,该函数接受一个List<Animal>类型的参数。 在makeSounds函数中,我们使用for循环遍历animals列表,并对其中的每个Animal实例调用makeSound方法。由于Cat是Animal的子类,因此它也可以被视为Animal类型,因此我们可以将其添加到List<Animal>类型的变量中。 这里的List<Animal>类型就是协变的,因为我们可以将它的子类(如Cat)作为参数传递给makeSounds函数。 现在我们来看一下逆变。假设我们有一个接受Animal类型的参数的函数: ```kotlin fun takeAnimal(animal: Animal) { animal.makeSound() } ``` 我们可以将这个函数传递给另一个函数,该函数期望一个Cat类型的参数。在这种情况下,我们可以使用in关键字来表示逆变: ```kotlin fun main() { val cat: Cat = Cat() takeCat(cat) } fun takeCat(cat: Cat) { takeAnimal(cat) } fun takeAnimal(animal: Animal) { animal.makeSound() } ``` 在这个例子中,我们定义了一个takeCat函数,它接受一个Cat类型的参数。我们将这个函数传递给了takeAnimal函数,该函数期望一个Animal类型的参数。由于Cat是Animal的子类,因此我们可以将Cat类型的参数传递给takeAnimal函数。这里的takeAnimal函数是逆变的,因为它可以接受其超类型(如Animal)的参数。 最后,我们来看一下out和in关键字。我们可以在定义泛型类型参数时使用这些关键字来限制泛型类型参数的使用方式。out关键字用于声明泛型类型参数是协变的,in关键字用于声明泛型类型参数是逆变的。 例如,我们可以定义一个只允许读取的泛型接口: ```kotlin interface ReadOnlyContainer<out T> { fun getItem(): T } ``` 在这个例子中,我们使用out关键字来声明泛型类型参数T是协变的。这意味着我们只能从ReadOnlyContainer接口中获取T类型的值,而不能修改它。这样做的好处是可以使我们更加安全地使用泛型类型参数。 类似地,我们可以定义一个只允许写入的泛型接口: ```kotlin interface WriteOnlyContainer<in T> { fun setItem(item: T) } ``` 在这个例子中,我们使用in关键字来声明泛型类型参数T是逆变的。这意味着我们只能向WriteOnlyContainer接口中设置T类型的值,而不能获取它。这样做的好处是可以避免意外修改泛型类型参数的值。 总结一下,Kotlin中的泛型支持协变逆变,以及in和out关键字。使用协变逆变可以使我们更加灵活地使用泛型类型参数,而使用in和out关键字可以帮助我们更加安全地使用泛型类型参数。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

ShuSheng007

亲爱的猿猿,难道你又要白嫖?

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

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

打赏作者

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

抵扣说明:

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

余额充值