继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

Kotlin 范型之协变、逆变

fengzhizi715
关注TA
已关注
手记 72
粉丝 1.7万
获赞 594

一. 类(Class) 与类型(Type)

Kotlin 中类和类型是不一样的概念。

下图充分展示了它们的区别。

class vs type.png

二. 型变

型变是指类型转换后的继承关系。

Kotlin 的型变分为逆变、协变和不变。

2.1 协变

如果 A 是 B 的子类型,并且Generic<A> 也是 Generic<B> 的子类型,那么 Generic<T> 可以称之为一个协变类。

2.1.1 Java 上界通配符<? extends T>

Java 的协变通过上界通配符实现。

如果 Dog 是 Animal 的子类,但 List<Dog> 并不是 List<Animal> 的子类。
下面的代码会在编译时报错:

        List<Animal> animals = new ArrayList<>();
        List<Dog> dogs = new ArrayList<>();
        animals = dogs; // incompatible types

而使用上界通配符之后,List<Dog> 变成了 List<? extends Animal> 的子类型。即 animals 变成了可以放入任何 Animal 及其子类的 List。

因此,下面的代码编译是正确的:

        List<? extends Animal> animals = new ArrayList<>();
        List<Dog> dogs = new ArrayList<>();
        animals = dogs;

2.1.2 Kotlin 的关键词 out

上述代码改成 Kotlin 的代码:

fun main() {

    var animals: List<Animal> = ArrayList()
    val dogs = ArrayList<Dog>()
    animals = dogs
}

居然没有编译报错?其实,Kotlin 的 List 跟 Java 的 List 并不一样。

Kotlin 的 List 源码中使用了outout相当于 Java 上界通配符。

public interface List<out E> : Collection<E> {

    override val size: Int
    override fun isEmpty(): Boolean
    override fun contains(element: @UnsafeVariance E): Boolean
    override fun iterator(): Iterator<E>

    override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean

    public operator fun get(index: Int): E

    public fun indexOf(element: @UnsafeVariance E): Int

    public fun lastIndexOf(element: @UnsafeVariance E): Int

    public fun listIterator(): ListIterator<E>

    public fun listIterator(index: Int): ListIterator<E>

    public fun subList(fromIndex: Int, toIndex: Int): List<E>
}

类的参数类型使用了out之后,该参数只能出现在方法的返回类型。

2.1.3 @UnsafeVariance

但是,Kotlin List 的 contains、containsAll、indexOf 和 lastIndexOf 方法中,入参均出现了范型 E。并且使用 @UnsafeVariance 修饰。

正是由于 @UnsafeVariance 的修饰,打破了刚才的限制,否则会编译报错。

2.2 逆变

如果 A 是 B 的子类型,并且 Generic<B> 是 Generic<A> 的子类型,那么 Generic<T> 可以称之为一个逆变类。

2.2.1 Java 下界通配符<? super T>

Java 的逆变通过下界通配符实现。

下面的代码因为是协变的,无法添加新的对象。编译器只能知道类型是 Animal 的子类,并不能确定具体类型是什么,因此无法验证类型的安全性。

        List<? extends Animal> animals = new ArrayList<>();
        animals.add(new Dog()); // compile error

使用下界通配符之后,代码编译通过:

        List<? super Animal> animals = new ArrayList<>();
        animals.add(new Dog());

? super Animal 表示 Animal 及其父类 。所以 animals 可以接收所有 Animal 的子类添加至该列表中。

Java 的上界通配符和下界通配符符合 PECS 原则。

PECS 原则即 Producer Extends,Consumer Super 。如果参数化类型是一个生产者,则使用 <? extends T>;如果它是一个消费者,则使用 <? super T>。

其中,生产者表示频繁往外读取数据 T,而不从中添加数据。消费者表示只往里插入数据 T,而不读取数据。

2.2.2 Kotlin 的关键词 in

in相当于 Java 下界通配符。

abstract class Printer<in E> {

    abstract fun print(value: E): Unit
}

class AnimalPrinter: Printer<Animal>() {

    override fun print(animal: Animal) {
        println("this is animal")
    }
}

class DogPrinter : Printer<Dog>() {

    override fun print(dog: Dog) {
        println("this is dog")
    }
}

fun main() {

    val animalPrinter = AnimalPrinter()
    animalPrinter.print(Animal())

    val dogPrinter = DogPrinter()
    dogPrinter.print(Dog())
}

类的参数类型使用了in之后,该参数只能出现在方法的入参。

2.3 不变

默认情况下,Kotlin 中的泛型类是不变的。 这意味着它们既不是协变的也不是逆变的。

例如 MutableList,它可读可写,泛型没有使用inout

三. 总结

本文从 Kotlin 的类、类型引出了型变。介绍了 Kotlin 的协变、协变和不变的概念和特性,以及 Java 的上界通配符、下界通配符。

该系列的相关文章:
Kotlin 泛型之类型擦除

打开App,阅读手记
2人推荐
发表评论
随时随地看视频慕课网APP