1034 字
约 3 分钟
6
S04-J04 · 泛型:擦除、通配符与它的代价
适用读者:被
ClassCastException或"泛型数组不能创建"卡住的人。
前置:S04-B06。
实测环境:OpenJDK 21.0.12.1(反射实测,2026-11-05)。
覆盖旧文:—(E 驱动;旧素材未覆盖泛型)
覆盖扩展:E-S04-015(类型擦除)、E-S04-016(PECS)。
上一篇:S04-J03 · 类文件结构与字节码
下一篇:S04-J05 · SPI 与运行时可扩展。
1. 一句话结论
Java 的泛型是"编译期检查 + 运行期擦除":编译器帮你检查类型,编译完泛型参数就被抹掉了。 所以运行时 List<String> 与 List<Integer> 是同一个类。
实测:
1. List<String> 与 List<Integer> 运行时类相同? true
2. 泛型参数(反射可见): [java.util.List<java.lang.String>]
3. 擦除后实际参数类型: [interface java.util.List]
4. 绕过泛型检查后 ls = [123](编译期警告,运行期不报)
2. 擦除的三个后果(E-S04-015)
| 后果 | 说明 |
|---|---|
| 运行时无类型信息 | 不能 instanceof List<String>、不能 new T[] |
| 反射可绕过检查 | 实测第 4 条:塞进 Integer 也不报错 |
| 重载冲突 | f(List<String>) 与 f(List<Integer>) 擦除后签名相同 → 编译错误 |
为什么这么设计:向后兼容——泛型是 Java 5 引入的,擦除让新代码能跑在旧 JVM 上,也让旧库无需重写。
| 语言 | 泛型实现 |
|---|---|
| Java | 擦除(运行期无信息) |
| C# | 具化(运行期有信息,可 new T[]) |
| Go 1.18+ | 泛型(编译期实例化,待核细节) |
3. 通配符与 PECS(E-S04-016)
List<? extends Number> src // 只能读(生产者)
List<? super Integer> dst // 只能写(消费者)
PECS = Producer Extends, Consumer Super
| 场景 | 用什么 | 能做什么 |
|---|---|---|
| 只读(从集合取) | ? extends T |
读 ✅ 写 ❌ |
| 只写(往集合放) | ? super T |
写 ✅ 读只能当 Object |
| 读写都要 | 具体类型 T |
都行 |
经典应用:Collections.copy(List<? super T> dest, List<? extends T> src)。
4. 泛型与数组:为什么不兼容
T[] arr = new T[10]; // ❌ 编译错误(擦除后不知道 T 是什么)
List<T>[] lists = ...; // ❌ 同理
| 替代方案 | 说明 |
|---|---|
List<T> |
优先用集合代替数组 |
Array.newInstance(clazz, n) |
反射创建(需传 Class) |
@SuppressWarnings("unchecked") |
强制转换(要注释说明为什么安全) |
根因:数组是协变且保留运行时类型(String[] 是 Object[] 的子类型,且能检查 ArrayStoreException);泛型是不变且擦除。两者语义冲突,所以不能混。
5. 实践建议
| 建议 | 原因 |
|---|---|
不要用原始类型(List 而非 List<?>) |
丢失全部编译期检查 |
优先 List<T> 而非 T[] |
避开数组与泛型的冲突 |
| 通配符只在API 签名用 | 内部实现用具体类型更简单 |
| 强制转换处写注释 | @SuppressWarnings 要有理由 |
6. 五个误区
| 误区 | 真相 |
|---|---|
| "泛型在运行时生效" | 编译期检查,运行期擦除 |
"instanceof List<String> 可以" |
不可以(擦除) |
| "泛型能创建数组" | 不能(用反射或集合) |
"? extends 既能读又能写" |
只能读 |
| "原始类型只是警告" | 会丢检查,可能在运行期炸 |
7. 自检
- 为什么
List<String>与List<Integer>运行时是同一个类? - 反射为什么能绕过泛型检查?
- PECS 分别对应什么场景?
- 为什么泛型不能创建数组?
- 原始类型(
List)有什么风险?
8. 下一篇
S04-J05 · SPI 与运行时可扩展系统设计
泛型讲完,下一篇讲"框架怎么做到加个 jar 就生效"——ServiceLoader 与实测的两个实现被发现。
本篇属于系列《S04 · Java 语言核心》。扩展点 E-S04-015/016。四条反射实测为 OpenJDK 21。
S04-J04 · 泛型:擦除、通配符与它的代价
https://www.clxhxhhr.top/posts/5227/ 评论
0 条
还没有评论,先写一条吧。