java泛型采用不变性设计,禁止list赋值给list(即使y extends x),以保障编译期类型安全;类型擦除使运行时无法校验,故通过不变性+通配符(pecs)实现安全复用。

不能赋值,是因为 Java 泛型设计上采用“不变性”(invariance),不是因为写法错或编译器 bug,而是为了守住类型安全底线。
泛型不是协变的,List ≠ List 的子类型
即使 Y extends X,List
- Java 编译器不会把 List
当作 List 的子类,哪怕 Dog 继承 Animal - 这种设计叫“不变”,即泛型参数必须严格一致才能赋值
- 根本目标是防止运行时 ClassCastException:比如往本该只存 Dog 的列表里误加 Cat
类型擦除让运行时无法做安全检查
泛型信息在编译后全被擦掉,List
- 如果允许 List
= new ArrayList (),那方法内部 add(new Cat()) 就会悄悄混入不兼容对象 - 后续 get(0) 强转成 Dog 时崩溃,错误拖到运行时才暴露,违背泛型“提前报错”的初衷
- 编译期拦截,就是用不变性堵住这个漏洞
想安全地复用,得用通配符代替硬转换
直接赋值被禁,但 Java 提供了更严谨的替代方案:通配符 + PECS 原则(Producer Extends, Consumer Super)。
- 只读场景 → 用 List extends X>:能接收 List
、List 等任意 X 子类列表,get() 返回 X 类型,但 add() 被禁止(避免混入不兼容类型) - 只写场景 → 用 List super X>:能接收 List
、List - 例如:
List extends Animal> animals = new ArrayList<dog>();</dog>合法且安全
别拿数组和泛型比,它们机制完全不同
数组支持协变(String[] 是 Object[] 的子类),但这是个历史包袱,导致运行时要多一层类型检查(如 ArrayStoreException)。泛型从设计之初就拒绝走这条路。
- 数组协变带来运行时开销和潜在风险;泛型选择用编译期严格检查换掉运行时不确定性
- Kotlin 的 List
看似能协变,其实是通过声明式协变(out 修饰符)实现的,底层仍靠编译器生成桥接逻辑,和 Java 的 raw type 擦除本质不同 - Java 保持统一语义:泛型类型安全由编译器全权负责,不依赖运行时补救
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











