泛型的三种型变类型:逆变,协变和不变

1. 一句话直观记忆

  • 协变(out) 出来安全(只读生产者)。如果 Cat <: Animal,那 Box <: Box

  • 逆变(in) 进去安全(只写消费者)。如果 Cat <: Animal,那 Box <: Box

  • 不变 :两边都不放过;Box 与 Box 没有子类型关系。

换成口号:生产者用 extends/out;消费者用 super/in;默认不变


2. 形式化定义(看"子类型关系如何传递")

设 S <: T(S 是 T 的子类型),则:

  • 协变: 方向保持
  • 逆变: 方向相反
  • 不变: 无子类型关系

3. Java 的三种型变(用通配符实现)

Java 的泛型默认不变;通过通配符 ? extends / ? super 在**使用处(use-site)**表达协/逆变。

3.1 协变:? extends T(只读生产者)

ini 复制代码
List<? extends Number> src = List.of(1, 2, 3);
// src.add(4); // ❌ 编译错误:不能安全写入
Number n = src.get(0);    // ✅ 读取安全(最小上界是 Number)

场景 :把 List 当作 List<? extends Number> 来读取元素。

代价:编译器禁止 add,因为你可能传入 Double 等破坏具体子类型。

3.2 逆变:? super T(只写消费者)

ini 复制代码
List<? super Integer> dst = new ArrayList<Number>();
dst.add(123);         // ✅ 可以写入 Integer(以及其子类)
Object x = dst.get(0); // 只能当 Object 读出(类型不精确)

场景 :把 List 当作 List<? super Integer> 来接收整数写入(消费者)。

代价:读出的精确类型丢失(只能是 Object)。

3.3 不变(默认)

ini 复制代码
List<Integer> a = new ArrayList<>();
List<Number>  b = a; // ❌ 不成立(不变)

结论:默认不变可以在编译期阻止很多潜在类型错配。

3.4 经典法则(PECS)

Producer Extends, Consumer Super****

  • 生产者(只读来源)→ ? extends T

  • 消费者(只写目标)→ ? super T

实战例:复制函数

javascript 复制代码
static <T> void copy(List<? extends T> from, List<? super T> to) {
  for (T e : from) to.add(e);
}

4. Kotlin 的型变(声明处 + 使用处)

Kotlin 在**声明处(declaration-site)**就能标注型变,更易用。

4.1 声明处协变out

kotlin 复制代码
interface Source<out T> { fun get(): T }
// 因为只"产出 T",声明为 out T → Source<Cat> 可当作 Source<Animal>
  • 约束:标了 out 的位置不能当参数使用(除非在类型投影等安全场合)。

4.2 声明处逆变in

kotlin 复制代码
interface Sink<in T> { fun put(x: T) }
// 因为只"消费 T",声明为 in T → Sink<Animal> 可当作 Sink<Cat>
  • 约束:标了 in 的位置不能作为返回类型(否则返回类型不安全)。

4.3 使用处投影(与 Java 通配符类似)

  • 协变投影:List 等价 Java List<? extends Number>

  • 逆变投影:MutableList 等价 Java List<? super Integer>

4.4 Kotlin 函数类型的型变(非常重要)

函数类型是参数逆变,返回协变

kotlin 复制代码
// (P) -> R  等价于 Function1<in P, out R>
val f: (Animal) -> Cat
val g: (Cat) -> Animal
// g 可以赋给 f 吗?参数逆变 → 需要 (super Cat);返回协变 → 需要 (sub Cat)
// 正确结论: (Animal)->Cat 可以赋给 (Cat)->Animal ? 反之更常见:
// 接口 Comparator<in T> / Producer<out T> 源于这一原则

更实用的例子:Comparator(只消费待比较对象,逆变);Iterable(只生产元素,协变)。

4.5 星投影

当你"不关心具体类型参数"时使用:

kotlin 复制代码
val anyList: List<*> = listOf(1, 2, 3) // 只能以 Any? 读取,不可写

5. 数组的特例(高频面试点)

  • Java 数组协变 的:Cat[] 可赋给 Animal[]。但这不类型安全,会在运行时抛出 ArrayStoreException:
ini 复制代码
Animal[] a = new Cat[10];
a[0] = new Dog(); // 运行期 ArrayStoreException
  • Kotlin Array 默认不变 ;需要型变时用 Array / Array 的投影

6. 什么时候用哪种型变?

需求 Java 写法 Kotlin 写法 说明
只读数据源(生产者) List<? extends T> List / 声明处 out 可读不可写
只写接收端(消费者) List<? super T> MutableList / 声明处 in 可写不可精确读
同时读写(容器、缓冲) 具体 List(不变) MutableList(不变) 默认不变更稳
API 抽象的只读接口 Iterable<? extends T> Iterable 设计层面协变
比较/回调(只消费) Comparator<? super T> Comparator 逆变更宽松
函数类型 N/A (in P) -> out R 记住"参逆返协"

7. 实战模式与坑点

7.1 只读接口与可变实现分离(Kotlin/Android 推荐)

kotlin 复制代码
interface Repo {
  val items: List<Item> // 只读暴露(协变接口)
}
class RepoImpl : Repo {
  private val _items = mutableListOf<Item>()
  override val items: List<Item> get() = _items // 外部看不到可变方法
}
  • 对外协变只读 ,对内持有可变实现,既安全又易维护。

7.2 复制/转换 API:PECS 套路

javascript 复制代码
// Java:from 生产(extends),to 消费(super)
static <T> void copy(List<? extends T> from, List<? super T> to) {
  for (T e : from) to.add(e);
}

7.3 比较器/回调要用逆变,兼容更广

ini 复制代码
Comparator<? super Cat> cmp = Comparator.comparing(Animal::getAge);
// 可拿比较 Animal 的比较器来排 Cat,更灵活

7.4 避免"错误协变"导致的写入失效

  • List<? extends Number> 看似"广",却不能 add ;若你既要读又要写,就不要用 extends/out,而是用不变 List 或把写入端单独抽出去。

8. 面试/复盘小抄(TL;DR)

  • 协变 :读安全、写受限------生产者(Java ? extends / Kotlin out)。

  • 逆变 :写安全、读不精确------消费者(Java ? super / Kotlin in)。

  • 不变:默认、稳妥------读写都要时用它。

  • PECS:Producer Extends,Consumer Super。

  • 函数类型参数逆变、返回协变

  • 数组:Java 协变但不安全(会 ArrayStoreException);Kotlin Array 不变。

  • API 设计:只读接口协变、消费者接口逆变;实现细节保持不变以便读写。

相关推荐
南北是北北2 小时前
list并发与共享
面试
ShooterJ4 小时前
Mysql小表驱动大表优化原理
数据库·后端·面试
小时前端4 小时前
🚀 面试必问的8道JavaScript异步难题:搞懂这些秒杀90%的候选人
javascript·面试
Takklin4 小时前
JavaScript 面试笔记:作用域、变量提升、暂时性死区与 const 的可变性
javascript·面试
知其然亦知其所以然4 小时前
面试官一开口就问:“你了解MySQL水平分区吗?”我当场差点懵了……
后端·mysql·面试
老马啸西风4 小时前
力扣 LC27. 移除元素 remove-element
算法·面试·github
南北是北北5 小时前
List排序/查找最佳实践
面试
南北是北北5 小时前
List视图与不可变
面试
绝无仅有5 小时前
面试技巧之Linux相关问题的解答
后端·面试·github