? extends T 天然只读,因编译器无法确定具体子类型,为保类型安全禁止非 null 写入;仅允许 get()、size() 等读操作,支持返回值向上转型为 T;典型用于只读工具方法如 sum(List<? extends Number>)。

Java 中用 ? extends T 实现只读集合,本质是通过编译期类型检查切断写入能力,保留安全读取权限。它不是靠运行时拦截,而是让非法写操作在编译阶段就失败。
为什么 ? extends T 天然就是只读的
因为编译器无法确定容器实际承载的是哪个具体子类型——可能是 Integer,也可能是 Double,甚至 BigDecimal。为保障类型安全,唯一稳妥策略就是禁止所有非 null 的写入操作。
-
允许的操作:调用
get()、size()、isEmpty()、iterator()等不改变内容的方法;返回值可安全向上转型为T(如Number) -
禁止的操作:
add()、addAll()、set()(除null外);任何试图插入具体对象的语句都会编译报错 -
关键细节:即使传入一个
new T()或其子类实例,也会被拒绝——不是因为值不对,而是“容器身份不确定”,写入即可能破坏一致性
典型只读场景与写法
常见于工具方法设计,目标是接收任意子类型集合,但只做遍历、计算或转换,不修改原数据。
- 求数值总和:
public static double sum(List<? extends Number> numbers)→ 可传List<Integer>或List<Double>,内部用num.doubleValue() - 统一打印:
public static void printShapes(List<? extends Shape> shapes)→ 调用shape.draw()安全,因所有子类都继承自Shape - 类型无关校验:
public static boolean hasNull(Collection<?> coll)→ 用无界通配符,仅需coll.contains(null)
和普通泛型 List<T> 的关键区别
表面上都是“能装 T 及其子类”,但行为截然不同:
立即学习“Java免费学习笔记(深入)”;
-
List<Shape>:可存Circle、Rectangle,也可混存;支持add(new Circle());是具体类型容器 -
List<? extends Shape>:指向的是某个**固定但未知的子类型**(比如底层实为List<Circle>),所以加Rectangle就越界;是受限只读视图 - 二者不能互相赋值:前者是“能装所有子类”的容器,后者是“只读某一种子类”的引用,语义不同
容易踩的坑
这些错误看似合理,实则违反只读契约:
- 误以为
add(null)是漏洞——其实它是唯一被允许的写操作,因null是所有引用类型的合法值 - 试图强制转型后写入:
((List<Circle>) list).add(new Circle())→ 编译失败,类型系统直接阻止 - 混淆上界与具体类型:
List<? extends String>没有意义,因String是final类,无子类,但仍遵循只读规则


















