【Kotlin】Kotlin协变和逆变

本文介绍了Kotlin中泛型相关的协变和逆变,和reified关键字
协变(Covariance)、逆变(Contravariance)和 reified 关键字是 Kotlin 泛型系统中比较高级和强大的特性。它们能帮助你编写更健壮、更灵活、更类型安全的泛型代码,尤其是在处理集合、高阶函数以及需要运行时类型检查的场景。
类型擦除
在深入协变和逆变之前,先简单回顾一下 Java/Kotlin 泛型的类型擦除(Type Erasure)。
在 JVM 上,泛型信息只在编译时存在,运行时会被擦除。这意味着 List<String> 和 List<Int> 在运行时都会变成 List<Object>(或 List<Any?>)。
这就导致了两个主要限制:
- 你不能在运行时直接获取泛型参数的具体类型(比如
T::class.java)。 - 你不能直接创建泛型数组(比如
Array<T>())。
Kotlin 中通过 reified 关键字解决了第一个限制,在内联函数中使用,可以在编译期就确定泛型参数的实际类型。
而 协变和逆变 则解决了在使用泛型时如何安全地处理子类型关系的问题。
Java中的协变和逆变
首先回顾下Java中是怎么做的, Java 泛型中的 super 和 extends 通配符,与 Kotlin 的协变 (out) 和逆变 (in) 概念密切相关。
Java 泛型通配符:extends 和 super
在 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。
上面的例子中, TextView 是 Button 的父类型 ,也就能够满足 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是特定子类型)。
- 这意味着,如果
A是B的子类型,那么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是特定父类型)。
- 这意味着,如果
A是B的子类型,那么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中的应用场景
- 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) 更简洁 - 启动 Activity: 简化 Activity 的启动,无需在
Intent中指定Class。inline fun <reified T : Activity> Context.startActivity() { startActivity(Intent(this, T::class.java)) } // 使用:context.startActivity<DetailActivity>() - 获取 Service: 简化获取系统服务。安卓热门网络请求库Retrofit也是使用了这个方法来示例化定义好的api服务的。
inline fun <reified T> Context.getSystemService(): T? { return getSystemService(T::class.java) as? T } // val locationManager = context.getSystemService<LocationManager>() - 查找视图: 在一些旧的视图查找框架中,可以使用
reified简化类型转换。
注意事项:
reified只能用于inline函数。- 由于内联的特性,过度使用
reified可能会导致生成的字节码文件变大。应合理使用。