泛型与类型擦除:编译期安全,运行期擦除
更新时间:2026-09-20。泛型让集合/容器在编译期就约束元素类型,把
ClassCastException提前到编译期。Java 为兼容 5.0 之前的代码采用类型擦除实现,这解释了一连串「为什么不能这样写」。
一、泛型类与方法
java
class Box<T> { // T 是类型参数
private T value;
void set(T v) { this.value = v; }
T get() { return value; }
}
Box<String> b = new Box<>(); // 钻石运算符 <> 推断
b.set("hi");
String s = b.get(); // 无需强转,编译期保证是 String
static <E> E first(E[] arr) { return arr[0]; } // 泛型方法,<E> 声明在返回值前编译后 Box<T> 里的 T 被擦除成其上界(默认 Object),运行时只剩 Box。因此:
- 不能
new T()、不能new T[](运行时没有 T 的具体类型)。 - 不能用基本类型作类型参数:
Box<int>非法,要用Box<Integer>(装箱,见基本类型与包装类)。 getClass()对Box<String>和Box<Integer>返回同一个Box.class。- 静态字段/方法不能使用类的类型参数(类型属于实例泛型,静态不沾光)。
二、不变、协变与通配符
泛型默认不变(invariant):List<String> 不是 List<Object> 的子类,哪怕 String extends Object。需要「某个范围的类型」用通配符:
| 写法 | 含义 | 能读成 | 能写入 |
|---|---|---|---|
List<? extends Number> | 上界:Number 或其子类(生产者) | Number | 不能 add(除 null) |
List<? super Integer> | 下界:Integer 或其父类(消费者) | Object | 可 add Integer 及其子类 |
List<?> | 任意类型 | Object | 不能 add |
java
// 生产者:从集合里「取」Number 出来用 —— extends
double sum(List<? extends Number> nums) {
double r = 0;
for (Number n : nums) r += n.doubleValue();
return r;
}
sum(List.of(1, 2, 3)); // List<Integer> 也能传
// 消费者:往集合里「塞」Integer —— super
void addInts(List<? super Integer> sink) {
sink.add(1); sink.add(2);
}
addInts(new ArrayList<Number>());三、PECS 原则

Producer Extends, Consumer Super。经典例子 Collections.copy(src, dest):源是生产者用 <? extends T>,目标是消费者用 <? super T>。
四、桥接方法与运行时
为在擦除后保留多态,编译器对「泛型类/接口 + 重写」合成桥接方法(bridge method),例如实现 Comparable<T> 后字节码里既有你写的 compareTo(MyClass),也有合成的 compareTo(Object) 转发。理解它能解释反射里为何多出一个方法。
坑
List<? extends T>想 add:编译器拒绝,因为不知道元素确切子类型;要写入应改用super或不加通配符。- 泛型当协变用:
List<String>传不进List<Object>形参,这是有意防止「把 Integer 塞进 String 列表」。 - 堆污染 heap pollution:通过原生类型(raw type)绕过检查会埋下 ClassCastException,避免混用原始
List与参数化List<String>。 instanceof T非法:运行时 T 已擦除;要判断类型需传Class<T>token。- varargs 泛型告警:泛型可变参数可能污染,工具方法可加
@SafeVarargs。
总结
泛型在编译期保证类型安全、在运行时通过类型擦除把类型参数替换为上界(默认 Object),所以不能 new T、不能用基本类型、getClass 相同;泛型默认不变,需要复用就靠通配符,并牢记 PECS——只往外读的生产者用 ? extends T、只往里写的消费者用 ? super T、又读又写就用裸 T;编译器还会为泛型重写合成桥接方法以维持多态。