深入理解Java与Kotlin的泛型(Generic Type)和型变(Variance)

有很多编程语言(尤其是面向对象语言)都有子类型(SubType)的概念,通过这一概念可以让我们在业务上实现一种阶级。”A Cat is-An Animal”。提现在代码中可以如下所示:
Java

Integer integer = new Integer(1);
Number number = integer;

Kotlin

val int: Int = 10
val number: Number = int

甚至于我们可以用一种更为抽象的方式去定义一个方法

void printNumber(Number number)

只要是Number或者Number的子类型(SubType)都可以以参数的方式传递给此方法,如下所示:

printNumber(integer);
printNumber(number);

但是由于Liskov替换原则, 在实际开发中处理这种子类型与副类型之间的赋值操作的时候经常会遇到各种问题。
比如以下Java代码是无法编译通过的

List<Integer> integerList = new ArrayList<>();
List<Number> numberList = integerList;    // 编译报错

如果要深入理解这里面的玄机,我们需要先了解一下主要概念:型变(Variance)

型变(Variance)

在Java和Kotlin中的型变包含3中类型的XX-Variance

 1. 协变(covariance)
 2. 逆变(contravariance)
 3. 不变(invariance)

首先来看一下对这几个概念的定义:
逆变、协变和不变都是用来描述类型转换(type transformation)后的继承关系,其定义:如果A、B表示类型,f(⋅)表示类型转换,≤表示继承关系(比如,A≤B表示A是由B派生出来的子类)

当A≤B时有f(A)≤f(B)成立,则f(⋅)是协变(covariant)的
当A≤B时有f(B)≤f(A)成立,则f(⋅)是逆变(contravariant)的
当A≤B时上述两个式子均不成立,即f(A)与f(B)相互之间没有继承关系,f(⋅)是不变(invariant)的

接下来我们通过创建几个类来做演示型变,如下所示

abstract class Animal(val size: Int)
class Dog(val cuteness: Int): Animal(100)
class Cat(val terrorFactor: Int): Animal(1)

可以看到,Animal是一个抽象父类,DogCat都是继承自Animal的子类,通过这几个类我们演示Java和Kotlin的型变区别,如下所示:

协变(covariance)

在Java中,当覆盖(Overriding)一个方法时, 这个方法必须是协变的, 也就是说被覆盖方法覆盖方法的返回参数类型必须在继承方向上一致。比如AnimalDoctor中的方法treat:Animal(为了简便,使用Kotlin的书写规范)可以被CatDoctor中的treat():Cat方法所覆盖。

Java

数组 可以编译通过

Object [] arr = new String [] {"hello", "world"};

普通对象 可以编译通过

Cat cat = new Cat();
Animal animal = cat;

泛型对象 编译报错!

List<Cat> cats = new ArrayList<>();
List<Animal> animals = cats;

可以看到Java对于以上三种类型,数组和普通对象编译器可以直接编译成功,但是对于最后一种泛型对象,编译器是会报错的,错误信息如下:
这里写图片描述
根据协变(covariance)的定义:当A≤B时有f(A)≤f(B)成立,则f(⋅)是协变(covariant)的。我们可以得出因为String和Cat依次是Objec和Animal的子类型,并且将String数组和Cat对象赋值给Object数组和Animal对象可以成功编译,则我们可以说Java对于数组和普通对象是支持协变的,但是对于带有泛型类型的List则不支持协变

而这也就意味着,以下代码是不能编译成功的

public static void main(String[] args) {
    List<Cat> cats = new ArrayList<>();
    //  playAnimal(cats);  // 编译报错
}

public static void playAnimal(List<Animal> animal) {
    ...
}

因为playAnimal接受的是List<’Animal’>,而List<’Cat’>并不是List<’Animal’>的子类型

Kotlin

数组 编译报错!

// 数组
val dogArr: Array<Dog> = arrayOf(Dog(1), Dog(2))
val animalArr: Array<Animal> = dogArr

普通对象 可以编译通过

val dog: Dog = Dog(10)
var animal: Animal = dog

泛型对象 需要先我们创建一个class叫ReadableList, 如下所示:

class ReadableList<T>{

}

很简单只是一个ReadableList并指定泛型T,之后编译如下代码同Java一样是会报错的

val dogReadable: ReadableList<Dog> = ReadableList()
val animalReadable: ReadableList<Animal> = dogReadable

报错信息也同Java一致
这里写图片描述

同样可以看出Kotlin和Java有点区别,对于数组Kotlin也是不支持协变(covariance)的
但是如果使用的是Kotlin系统API中的List,则以下代码正常编译:

val dogList: List<Dog> = listOf(Dog(10), Dog(20))
playAnimal(dogList)

fun playAnimal(animalList: List<Animal>) {
    ...
}

但是如下代码却是无法编译通过的

val catList: Array<Cat> = arrayOf<Cat>(Cat(10), Cat(20))
playAnimal(catList)

fun playAnimal(animalList: Array<Animal>) {
    ...
}

至于为什么Kotlin对数组默认不支持协变,但是对List<泛型>是支持协变的,我们稍后再讲解

如何使Java和Kotlin添加协变支持

在Java和Kotlin中可以通过一定的方式对默认不支持协变的参数类型添加支持。 但是Java和Kotlin这两种语言处理的方式不同

Java : use-site variance(使用端型变)
Kotlin : declaration-site variance(声明端型变/定义端型变)

可以看出Java使用的是使用端型变,而Kotlin使用的是声明端型变。这两者有什么区别呢?

Java

个人理解就是使用端型变(use-site variance)就是在具体使用(初始化)某一个Class的对象时进行协变,具体如下代码所示:

List<Cat> catList = new ArrayList<>();
List<? extends Animal> animalList = catList;

可以看到,在我们声明animalList时,对泛型进行了一点修改,使用? extends Animal进行修饰之后,以上代码就可以成功编译并运行了。甚至于我们可以定义一个方法来接受这种参数类型,如下所示:

List<Cat> cats = new ArrayList<>();
playAnimal(cats);

public static void playAnimal(List<? extends Animal> animal) {
    ...
}

编译可以顺利通过, 这样我们的代码可扩展性会更高!

Kotlin

Kotlin使用的是声明端型变(declaration-site variance), 意思就是说,在定义一个类的时候处理, 具体如下所示:

// 使用out关键字
class ReadableList<out T>{

}

val dogReadable: ReadableList<Dog> = ReadableList()
val animalReadable: ReadableList<Animal> = dogReadable

以上代码跟之前唯一的不同点就是我们在定义ReadableList是对泛型添加了一点限制 out, 然后就可以将dogReadable顺利的赋值给animalReadable对象. 看到这我们应该就能猜到为什么之前Kotlin API中的List<泛型>是支持

注意:但是使用out关键字修饰了之后,在ReadableList类内部不可以有以T为参数类型的方法,比如我们在ReadableList中添加一个方法如下:
fun setMethod(t: T) {

}

这个时候,编译器会编译失败,并且告诉我们失败原因如下:
这里写图片描述

意思就是说这个“T”已经被声明为out, 所以在这个类中不能有以”T”为参数的函数

总结

当我们去研究理解型变的概念的时候,需要从这几个方向去下手:数组、普通对象、泛型对象。意思就是说我们要验证在Java和Kotlin中对着3个方向的支持程度.
用几句白话文做一下总结就是:

1 Java和Kotlin对普通对象都支持协变
2 Java对于数组支持协变,但是Kotlin对于数组不支持协变
3 Java和Kotlin默认都是不支持泛型类型协变的,但是两者皆可以通过一定的方式进行支持:Java -> use-site variance, Kotlin -> declaration-site variance.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值