
深入理解 Java/Kotlin 协变与逆变
型变(Variance)是泛型编程中最容易让人困惑的概念之一。本文从问题出发,讲清原理,落到实战。 型变(Variance)是泛型编程中最容易让人困惑的概念之一。本文从问题出发,讲清原理,落到实战。 直觉上 Cat 是 Animal 的子类,List<Cat> 应该也能赋值给 List<Animal>。但编译器拒绝了——为什么? 如果允许这种赋值,类型安全就会被破坏。 这就是"型变"要解决的问题:如何在保持类型安全的前提下,让泛型也能享受继承带来的灵活性。 假设 Cat extends Animal,对于泛型 Box<Cat> 和 Box<Animal>: Java 数组是协变的,这在编译期不会报错,但运行时可能崩溃: 数组在运行时仍记得自己是 Cat[],存入 Dog 时抛出异常。这是 Java 早期为了兼容没有泛型的代码而做的妥协,后来被泛型的"不变"设计纠正。 Java 泛型默认不变,灵活性靠使用处型变(在使用泛型的地方声明): 口诀:extends 只能读,super 只能写。 Producer Extends, Consumer Super —— 生产者用 extends,消费者用 super。 Producer Extends, Consumer Super —— 生产者用 extends,消费者用 super。 JDK 经典案例 Collections.copy: Kotlin 认为 Java 的"每次使用都要写通配符"太啰嗦,于是把型变声明移到了类的定义处。 当类本身没有声明型变时,Kotlin 也支持在使用处投影: 设计启示:只读接口加 out,只写接口加 in,读写都有就保持不变。 Kotlin 函数类型的定义完美体现了 PECS: 记忆:参数逆变,返回值协变(函数类型也是"消费参数、生产结果")。
