kotlin 泛型协变

概述

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

定义

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

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

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

1
2
open class Animal 
class Dog : Animal()

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

1
2
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>的子类型。因为如果不这样的话就会产生类型不安全问题。

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

1
2
3
4
5
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中要优先使用泛型集合。

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

协变

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

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

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

1
boolean addAll(Collection<E> c);

那怎么办呢?

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

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

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

逆变

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

1
2
void m (List<? super String){
}

协变与逆变的特性

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

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

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

1
2
3
4
interface BoxJ<T> {
T getAnimal();
void putAnimal(T a);
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
//协变,可以接受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之协变(Covariance)逆变(Contravariance)与抗变(Invariant),一定要先阅读这一篇文章,再阅读本文,不然看不懂!

上篇讲到Java中泛型是抗变的,但是数组却是协变的。Kotlin做的更彻底,不仅泛型是抗变的就连数组也变成抗变的了。

下面的代码是编译不过的

1
2
val strArray:Array<String> = arrayOf("shu","sheng","007")
val array:Array<Any> = strArray

报错:

1
Type mismatch: inferred type is Array<String> but Array<Any> was expected

泛型型变

官方文档见:Variance

Kotlin中没有通配符,取而代之的是 Declaration-site varianceUse-site variance 。其通过两个关键字outin来实现Java中的? extends? super 的功能.

假设我们有如下两个类和一个接口

1
2
3
4
5
6
7
open class Animal
class Dog : Animal()

interface Box<T> {
fun getAnimal(): T
fun putAnimal(a: T)
}

协变(out)

我们要定义一个方法,参数类型为Box<Animal>,但是我们希望可以传入Box<Dog>即希望可以发生协变

Java实现

1
2
3
4
5
private Animal getOutAnimalFromBox(Box<? extends Animal> box) {
Animal animal = box.getAnimal();
// box.putAnimal(? ) 没有办法调用修改方法,因为我们不知道?究竟是一个什么类型,没办法传入
return animal;
}

Kotlin对应的实现为:

1
2
3
4
5
fun getAnimalFromBox(b: Box<out Animal>) : Animal {
val animal: Animal = b.getAnimal()
// b.putAnimal(Nothing) 无法调用,因为方法需要一个Nothing类型的对象,但是在kotlin中无法获取
return animal
}

此方法可以接受Box<Dog>类型的参数了。

可见此处使用out 代替了? extends。从结果来看确实更合适一点,因为传入的参数只能提供值,而不能消费值。由于out是在方法调用的参数中标记的,处于使用端,所以叫Use-site varianceUse-site variance对应的就是Declaration-site variance了。

我们发现接口 Box<T>中既有消费值的方法fun putAnimal(a: T),又有提供值的方法fun getAnimal(): T,导致我们必须在使用侧告诉编译器我们要使用哪一类方法。那我们可以在声明接口的时候告诉编译器吗?答案是肯定的,但是就需要将接口拆分为只包含提供值的方法的接口producer与只包含消费值的方法的接口consumer

1
2
3
4
5
6
7
8
//producer
interface ReadableBox<out T> {
fun getAnimal(): T
}
//consumer
interface WritableBox<in T> {
fun putAnimal(a: T)
}

拆分完接口并做了相应的声明后,就可以不在使用端使用out或者in了。

1
2
3
fun getAnimalFromReadableBox(b: ReadableBox<Animal>){
val a: Animal = b.getAnimal()
}

上面的方法可以直接接受ReadableBox<Dog>类型的参数,给人的感觉好像是Kotlin使得泛型协变了。

1
2
3
4
5
getAnimalFromReadableBox(object :ReadableBox<Dog>{
override fun getAnimal(): Dog {
return Dog()
}
})

此种情况下outin是在声明时候使用的,所以叫Declaration-site variance了。

逆变(in)

我们要定义一个方法,参数类型为Box<Dog>,但是我们希望可以传入Box<Animal>,即希望可以发生逆变

Java实现

1
2
3
4
private void putAnimalInBox(BoxJ<? super Dog> box){
box.putAnimal(new Dog());
Object animal= box.getAnimal();// 可以调用读取方法,但是返回的类型确实Object,因为我们只能确定?的大基类是Object
}

Kotlin对应实现

1
2
3
4
fun putAnimalInBox(b: Box<in Dog>){
b.putAnimal(Dog())
val animal:Any? = b.getAnimal()// 可以调用读取方法,但是返回的类型确实Any?,因为我们只能确定?的大基类是Any?
}

此方法可以接受Box<Animal>类型的参数了

可见此处使用in 代替了? super,从结果来看确实更合适一点,因为传入的参数只适合消费值,而不适合获取值,获取到的值失去了有用的类型信息。由于in是在方法调用的参数中标记的,处于使用端,所以叫Use-site variance

让我们来看一下使用Declaration-site variance实现逆变

1
2
3
fun putAnimalToWritableBox(b:WritableBox<Dog>){
b.putAnimal(Dog())
}

上面的方法可以直接接受WritableBox<Animal>类型的参数,给人的感觉好像是Kotlin使得泛型逆变了。

1
2
3
4
putAnimalToWritableBox(object :WritableBox<Animal>{
override fun putAnimal(a: Animal) {
}
})

in & out 怎么记?

  • out 相当于java里面,in相当于

Out (协变)

如果你的类是将泛型作为内部方法的返回,那么可以用 out:

1
2
3
interface Production<out T> {
fun produce(): T
}

可以称其为 production class/interface,因为其主要是产生(produce)指定泛型对象。因此,可以这样来记:produce = output = out

In(逆变)

如果你的类是将泛型对象作为函数的参数,那么可以用 in:

1
2
3
interface Consumer<in T> {
fun consume(item: T)
}

可以称其为 consumer class/interface,因为其主要是消费指定泛型对象。因此,可以这样来记:consume = input = in。

Invariant(不变)

如果既将泛型作为函数参数,又将泛型作为函数的输出,那就既不用 in 或 out。

1
2
3
4
interface ProductionConsumer<T> {
fun produce(): T
fun consume(item: T)
}

举个例子

假设我们有一个汉堡(burger)对象,它是一种快餐,当然更是一种食物

1
2
3
open class Food
open class FastFood : Food()
class Burger : FastFood()

1. 汉堡提供者

根据上面定义的类和接口来设计提供 food, fastfoodburger 的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class FoodStore : Production<Food> {
override fun produce(): Food {
println("Produce food")
return Food()
}
}

class FastFoodStore : Production<FastFood> {
override fun produce(): FastFood {
println("Produce food")
return FastFood()
}
}

class InOutBurger : Production<Burger> {
override fun produce(): Burger {
println("Produce burger")
return Burger()
}
}

现在,我们可以这样赋值:

1
2
3
val production1 : Production<Food> = FoodStore()
val production2 : Production<Food> = FastFoodStore()
val production3 : Production<Food> = InOutBurger()

很显然,汉堡商店属于是快餐商店,当然也属于食品商店。

因此,对于 out 泛型,我们能够将使用子类泛型的对象赋值给使用父类泛型的对象。

而如果像下面这样反过来使用子类 - Burger 泛型,就会出现错误,因为快餐(fastfood)和食品(food)商店不仅仅提供汉堡(burger)。

1
2
3
val production1 : Production<Burger> = FoodStore()  // Error
val production2 : Production<Burger> = FastFoodStore() // Error
val production3 : Production<Burger> = InOutBurger()

2. 汉堡消费者

再让我们根据上面的类和接口来定义汉堡消费者类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Everybody : Consumer<Food> {
override fun consume(item: Food) {
println("Eat food")
}
}

class ModernPeople : Consumer<FastFood> {
override fun consume(item: FastFood) {
println("Eat fast food")
}
}

class American : Consumer<Burger> {
override fun consume(item: Burger) {
println("Eat burger")
}
}

现在,我们能够将 Everybody, ModernPeople 和 American 都指定给汉堡消费者(Consumer):

1
2
3
val consumer1 : Consumer<Burger> = Everybody()
val consumer2 : Consumer<Burger> = ModernPeople()
val consumer3 : Consumer<Burger> = American()

很显然这里美国的汉堡的消费者既是现代人,更是人类。

因此,对于 in 泛型,我们可以将使用父类泛型的对象赋值给使用子类泛型的对象。

同样,如果这里反过来使用父类 - Food 泛型,就会报错:

1
2
3
val consumer1 : Consumer<Food> = Everybody()
val consumer2 : Consumer<Food> = ModernPeople() // Error
val consumer3 : Consumer<Food> = American() // Error

根据以上的内容,我们还可以这样来理解什么时候用 in 和 out:

  • 父类泛型对象可以赋值给子类泛型对象,用 in;
  • 子类泛型对象可以赋值给父类泛型对象,用 out。

作者:极小光
链接:https://www.jianshu.com/p/c5ef8b30d768
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!