【Kotlin】Kotlin协变和逆变

【Kotlin】Kotlin协变和逆变

本文介绍了Kotlin中泛型相关的协变和逆变,和reified关键字

协变(Covariance)、逆变(Contravariance)和 reified 关键字是 Kotlin 泛型系统中比较高级和强大的特性。它们能帮助你编写更健壮、更灵活、更类型安全的泛型代码,尤其是在处理集合、高阶函数以及需要运行时类型检查的场景。

类型擦除

在深入协变和逆变之前,先简单回顾一下 Java/Kotlin 泛型的类型擦除(Type Erasure)

在 JVM 上,泛型信息只在编译时存在,运行时会被擦除。这意味着 List<String>List<Int> 在运行时都会变成 List<Object>(或 List<Any?>)。

这就导致了两个主要限制:

  1. 你不能在运行时直接获取泛型参数的具体类型(比如 T::class.java)。
  2. 你不能直接创建泛型数组(比如 Array<T>())。

Kotlin 中通过 reified 关键字解决了第一个限制,在内联函数中使用,可以在编译期就确定泛型参数的实际类型。

协变和逆变 则解决了在使用泛型时如何安全地处理子类型关系的问题。

Java中的协变和逆变

首先回顾下Java中是怎么做的, Java 泛型中的 superextends 通配符,与 Kotlin 的协变 (out) 和逆变 (in) 概念密切相关。

Java 泛型通配符:extendssuper

Java 中,泛型默认是 不变的 (invariant) ,这意味着 List<String> 并不是 List<Object> 的子类型,也就是说,子类的泛型(List<String>)不属于泛型(List<Object>)的子类,反之亦然。

为了在需要时放宽这种限制,Java 引入了泛型通配符? extends T? super T

它们允许你在泛型类型参数上定义上限或下限,从而实现 协变(Covariance)逆变(Contravariance) 的效果。

1. ? extends T (上界通配符)

简介:

  • 含义: ? extends T 表示“类型是 T 或 T 的某个子类型”。
  • 用途: 主要用于从泛型结构中读取数据。你可以从一个 List<? extends T> 中获取 T 类型的对象,但不能安全地向其中添加任何 T 类型的对象(除了 null)。
  • 角色: 充当生产者 (Producer)。如果你要从集合中东西,那么这个集合应该使用 extends
  • 与 Kotlin 的 out 对应: ? extends T 在 Java 中实现了协变

如果 Sub 是 Super 的子类型,那么 Generic<Sub> 也是 Generic<Super> 的子类型,就称为协变。即 子类型关系在泛型中得以保留。

List<Button> buttons = new ArrayList<Button>();
List<? extends TextView> textViews = buttons; // 合法
TextView textView = textViews.get(0); // 合法

// 下面的描述都是成立的
List<? extends TextView> textViews = new ArrayList<TextView>(); // 👈 本身
List<? extends TextView> textViews = new ArrayList<Button>(); // 👈 直接子类
List<? extends TextView> textViews = new ArrayList<RadioButton>(); // 👈 间接子类

前面说到 List<? extends TextView> 的泛型类型是个未知类型 ?,编译器也不确定它是啥类型,只是有个限制条件。

由于它满足 ? extends TextView 的限制条件,所以 get 出来的对象,肯定是 TextView 的子类型,根据多态的特性,能够赋值给 TextView,啰嗦一句,赋值给 View 也是没问题的。

List<? extends TextView> textViews = new ArrayList<Button>();
TextView textView = textViews.get(0); // 合法
View view = textViews.get(0); // 合法

// 下面的添加元素的代码是不合法的
textViews.add(new Button()); // 不合法
textViews.add(new TextView()); // 不合法

到了 add 操作的时候,我们可以这么理解:

  • List<? extends TextView> 由于类型未知,它可能是 List<Button>,也可能是 List<TextView>
  • 对于前者,显然我们要添加 TextView 是不可以的。
  • 实际情况是编译器无法确定到底属于哪一种,无法继续执行下去,就报错了。

2. ? super T (下界通配符)

简介:

  • 含义: ? super T 表示“类型是 T 或 T 的某个父类型”。
  • 用途: 主要用于向泛型结构中写入数据。你可以向一个 List<? super T> 中添加 T 类型的对象或其任何子类型,但从其中获取元素时,你只能确定它们是 Object 类型。
  • 角色: 充当消费者 (Consumer)。如果你要向集合中东西,那么这个集合应该使用 super
  • 与 Kotlin 的 in 对应: ? super T 在 Java 中实现了逆变

如果 Sub 是 Super 的子类型,那么 Generic<Super>Generic<Sub> 的子类型,就称为逆变。即 子类型关系在泛型中被反转。

先看一下它的写法:

List<? super Button> buttons = new ArrayList<TextView>();

这个 ? super 叫做「下界通配符」,可以使 Java 泛型具有「逆变性 Contravariance」。

与上界通配符对应,这里 super 限制了通配符 ? 的子类型,所以称之为下界。

它也有两层意思:

  • 通配符 ? 表示 List 的泛型类型是一个未知类型。
  • super 限制了这个未知类型的下界,也就是泛型类型必须满足这个 super 的限制条件。
    • super 我们在类的方法里面经常用到,这里的范围不仅包括 Button 的直接和间接父类,也包括下界 Button 本身。
    • super 同样支持 interface

上面的例子中, TextViewButton 的父类型 ,也就能够满足 super 的限制条件,就可以成功赋值了。

其他示例:

List<? super Button> buttons = new ArrayList<Button>(); // 👈 本身
List<? super Button> buttons = new ArrayList<TextView>(); // 👈 直接父类
List<? super Button> buttons = new ArrayList<Object>(); // 👈 间接父类

在涉及到拿取和添加元素的情景时,编译器可以确定你 添加进去的元素是 Button 的父类 ,Button 对象一定是这个未知类型的子类型,根据多态的特性,这里通过 add 添加 Button 对象是合法的。

但你不能通过 get 方法拿到这个元素,因为编译器只知道它是个未知类型,是 Button 的父类,但是你拿什么类型的对象来接收呢(除非Object)。

使用下界通配符 ? super 的泛型 List,只能读取到 Object 对象,一般没有什么实际的使用场景,通常也只拿它来添加数据,也就是消费已有的 List<? super Button>,往里面添加 Button,因此这种泛型类型声明称之为「消费者 Consumer」。

Kotlin中的 协变:out 关键字

以上为java中实现逆变和协变的方法,在Kotlin中的写法如何呢?在 Kotlin 中,当泛型类型参数被标记为 out 时,它表示该类型参数只能被生产(作为返回值输出),而不能被消费(作为参数输入)。(这个很形象,一个out,一个in)

  • 如果一个类 Producer<T> 的类型参数 T 被声明为 out
    • Producer<T> 的成员函数只能返回 T 类型的值
    • Producer<T> 的成员函数不能接受 T 类型的值作为参数(因为你无法保证传入的 T 是特定子类型)。
  • 这意味着,如果 AB 的子类型,那么 Producer<A> 就是 Producer<B> 的子类型。
// 声明一个协变接口:只能生产 T 类型
interface Producer<out T> {
    fun produce(): T // T 只能作为返回类型(生产)
    // fun consume(item: T) // 编译错误!T 不能作为参数类型(消费)
}

open class Animal
class Cat : Animal()
class Dog : Animal()

// 实现生产 Animal 的生产者
class AnimalProducer : Producer<Animal> {
    override fun produce(): Animal = Cat() // 可以生产 Cat (是 Animal 的子类)
}

// 实现生产 Cat 的生产者
class CatProducer : Producer<Cat> {
    override fun produce(): Cat = Cat()
}

fun main() {
    val animalProducer: Producer<Animal> = CatProducer() // 协变:CatProducer 可以被赋值给 Producer<Animal>
    val animal: Animal = animalProducer.produce() // produce() 返回 Animal
    println("Produced: $animal")
    // AnimalProducer producerCat = CatProducer() // 这样也是可以的
}

何时使用 out 当你的泛型类型只作为输出(例如,函数返回值、只读属性)时,使用 out。这通常用于表示“提供者”或“源头”。Kotlin 的 List<out E> 就是一个很好的例子:你只能从 List 中获取元素,不能添加特定类型的元素(尽管 MutableList<E> 不会使用 out,因为它可以添加)。

Kotlin中的逆变:in 关键字

在 Kotlin 中,当泛型类型参数被标记为 in 时,它表示该类型参数只能被消费(作为参数输入),而不能被生产(作为返回值输出)。

  • 如果一个类 Consumer<T> 的类型参数 T 被声明为 in
    • Consumer<T> 的成员函数只能接受 T 类型的值作为参数
    • Consumer<T> 的成员函数不能返回 T 类型的值(因为你无法保证返回的 T 是特定父类型)。
  • 这意味着,如果 AB 的子类型,那么 Consumer<B> 就是 Consumer<A> 的子类型。
// 声明一个逆变接口:只能消费 T 类型
interface Consumer<in T> {
    fun consume(item: T) // T 只能作为参数类型(消费)
    // fun produce(): T // 编译错误!T 不能作为返回类型(生产)
}

open class Animal
class Cat : Animal()
class Dog : Animal()

// 实现消费 Animal 的消费者
class AnimalConsumer : Consumer<Animal> {
    override fun consume(item: Animal) {
        println("Consuming an animal: $item")
    }
}

// 实现消费 Cat 的消费者
class CatConsumer : Consumer<Cat> {
    override fun consume(item: Cat) {
        println("Consuming a cat: $item")
    }
}

fun main() {
    val catConsumer: Consumer<Cat> = AnimalConsumer() // 逆变:AnimalConsumer 可以被赋值给 Consumer<Cat>
    catConsumer.consume(Cat()) // 可以消费 Cat
    // catConsumer.consume(Animal()) // 编译错误!因为 catConsumer 期望的是 Cat 或其子类型
}

何时使用 in 当你的泛型类型只作为输入(例如,函数参数、只写属性)时,使用 in。这通常用于表示“消费者”或“汇集点”。Kotlin 的 Comparator<in T> 就是一个很好的例子:它可以通过比较任何 T 或其超类型来比较 T

reified 关键字

最后介绍一下Kotlin中的reifeid关键字,reified 关键字用于 内联函数 (inline functions) 的泛型类型参数。它解决了 Java/Kotlin 泛型类型擦除的问题,允许你 在运行时访问泛型类型信息

由于类型擦除,你不能像下面的示例一样写,直接在运行时检查一个泛型类型:

// 这是不允许的,因为 T 在运行时是 Any/Object
fun <T> checkIfString(value: Any) {
    // if (value is T) { // 编译错误!Cannot check for instance of erased type: T
    //    println("It's a T")
    // }
}

// 也不允许获取 T 的 Class 对象
// fun <T> createInstance(): T {
//    return T::class.java.newInstance() // 编译错误!Cannot use T as reified type parameter
// }

reified 的作用

当一个泛型类型参数被标记为 reified 时,Kotlin 编译器会在编译时将该类型参数的具体类型信息内联到调用点。这意味着在运行时,该泛型类型不再被擦除,你可以像访问普通类型一样访问它。

  • reified 只能用于 inline 函数的类型参数。因为内联函数会将其代码复制到调用点,所以编译器有机会“知道”实际的类型参数。
  • 有了 reified,你就可以在函数体内使用 is 运算符、as 运算符以及 T::class.java
// 使用 reified 关键字检查类型
inline fun <reified T> T.checkClassType() {
    // 类型 T 内联解析
    when (this) {
        is Int -> {
            // 检查 this 是否为 Int 类型
            println("this is a Int: $this")
        }

        is String -> {
            // 检查 this 是否为 String 类型
            println("this is a String: $this")
        }

        else -> {
            // 检查 this 是否为其他类型
            println("this is a other type: $this")
        }
    }
}

/**
this is a Int: 2
this is a String: Kotlin
this is a other type: 2.0
*/

// 使用reified创建类实例
class Fish {
    fun swim() {
        println("Fish is swimming")
    }
}

inline fun <reified T> createInstance(){
    try {
        // 1. 获取 ClassLoader
        val classLoader = Thread.currentThread().contextClassLoader
        // 2. 加载类
        val className = T::class.java.name
        val loadedClass = classLoader?.loadClass(className)
        // 3. 创建实例
        val instance = loadedClass?.getDeclaredConstructor()?.newInstance()

        instance?.let {
            // 4. 调用方法
            val method = loadedClass.getDeclaredMethod("swim")
            method.invoke(instance)
        }
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

/**
Fish is swimming
*/

reified 在Android中的应用场景

  1. JSON 解析库: 许多 JSON 解析库(如 Gson, Moshi, kotlinx.serialization)的扩展函数使用 reified 来简化类型指定,无需传递 Class<T> 参数。
    // 假设你有这样一个扩展函数
    inline fun <reified T> String.fromJson(): T {
        // 内部使用 T::class.java 进行类型反序列化
        // ...
        throw NotImplementedError()
    }
    
    // val user = jsonString.fromJson<User>() // 比 jsonString.fromJson(User::class.java) 更简洁
    
  2. 启动 Activity: 简化 Activity 的启动,无需在 Intent 中指定 Class
    inline fun <reified T : Activity> Context.startActivity() {
        startActivity(Intent(this, T::class.java))
    }
    
    // 使用:context.startActivity<DetailActivity>()
    
  3. 获取 Service: 简化获取系统服务。安卓热门网络请求库Retrofit也是使用了这个方法来示例化定义好的api服务的。
    inline fun <reified T> Context.getSystemService(): T? {
        return getSystemService(T::class.java) as? T
    }
    
    // val locationManager = context.getSystemService<LocationManager>()
    
  4. 查找视图: 在一些旧的视图查找框架中,可以使用 reified 简化类型转换。

注意事项:

  • reified 只能用于 inline 函数。
  • 由于内联的特性,过度使用 reified 可能会导致生成的字节码文件变大。应合理使用。