Kotlin 范型之协变、逆变、不变

一. 前言

复制代码
Kotlin 中类和类型是不一样的概念。型变是指类型转换后的继承关系。Kotlin 的型变分为逆变、协变和不变。

二. 协变

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

2.1Java 上界通配符<? 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.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.3 @UnsafeVariance

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

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

三. 逆变

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

3.1.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,而不读取数据。

3.1.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之后,该参数只能出现在方法的入参。

四. 不变

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

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

五. 总结

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

相关推荐
北执南念2 分钟前
项目代码生成工具
java
中国lanwp8 分钟前
springboot logback 默认加载配置文件顺序
java·spring boot·logback
苹果酱056740 分钟前
【Azure Redis 缓存】在Azure Redis中,如何限制只允许Azure App Service访问?
java·vue.js·spring boot·mysql·课程设计
Java致死1 小时前
单例设计模式
java·单例模式·设计模式
胡子发芽1 小时前
请详细解释Java中的线程池(ThreadPoolExecutor)的工作原理,并说明如何自定义线程池的拒绝策略
java
沫夕残雪1 小时前
Tomcat的安装与配置
java·tomcat
胡子发芽1 小时前
请解释Java中的NIO(New I/O)与传统I/O的区别,并说明NIO中的关键组件及其作用
java
柚个朵朵2 小时前
IDEA中使用Git
java·git·spring
用户3031057186852 小时前
Kotlin协程的介绍
kotlin