Java 泛型擦除到底擦掉了什么?类型安全与桥接方法深度拆解
面试常考:类型擦除、桥接方法、通配符 PECS 原则、泛型不能做的事
一、从一个编译报错说起
java
// 这段代码编译报错,为什么?
public class GenericDemo<T> {
// private static T instance; // 编译错误: 不能在静态字段使用类型参数
// public boolean equals(T obj) { return false; } // 不会override Object.equals
private T value;
// 这两个方法能重载吗?
// public void set(String s) {}
// public void set(Integer i) {} // 编译错误: 类型擦除后签名冲突
}
Java 泛型在编译后会被"擦除",所有泛型类型在运行时都是同一个原始类型。类型擦除到底擦掉了什么?带来了哪些限制和陷阱?
二、类型擦除机制
擦除前 vs 擦除后
java
// 源码
public class Container<T> {
private T value;
public T get() { return value; }
public void set(T value) { this.value = value; }
}
Container<String> c1 = new Container<>();
Container<Integer> c2 = new Container<>();
System.out.println(c1.getClass() == c2.getClass()); // true! 都是 Container.class
java
// 擦除后 (字节码)
public class Container {
private Object value; // T → Object (无界擦除)
public Object get() { return value; }
public void set(Object value) { this.value = value; }
}
// 有界擦除
public class NumberContainer<T extends Number> {
private T value;
}
// 擦除后:
public class NumberContainer {
private Number value; // T extends Number → Number (有界擦除)
}
擦除规则
| 泛型声明 | 擦除后类型 |
|---|---|
T (无界) |
Object |
T extends Number |
Number |
T extends Comparable & Serializable |
第一个边界 Comparable |
List<String> |
List |
Map<K, V> |
Map |
擦除后的字节码验证
java
public class ErasureDemo {
public void demo() {
List<String> strings = new ArrayList<>();
strings.add("hello");
// 查看字节码: add 方法的参数类型是 Object
// get 方法的返回类型是 Object,前面有 checkcast String
}
}
less
字节码:
// strings.add("hello")
aload_1
ldc #3 // String hello
invokevirtual #4 // Method java/util/List.add:(Ljava/lang/Object;)Z
// String s = strings.get(0)
aload_1
iconst_0
invokeinterface #5 // Method java/util/List.get:(I)Ljava/lang/Object;
checkcast #6 // class java/lang/String ← 编译器插入的强制转换
astore_2
三、类型擦除带来的限制
1. 运行时无法获取泛型类型
java
public <T> void process(List<T> list) {
// 编译错误: T 是类型参数,不是 Class
// T.class
// new T()
// T instanceof String
// 只能传入 Class<T>
}
// 正确写法: 传入 Class<T>
public <T> T createInstance(Class<T> clazz) throws Exception {
return clazz.getDeclaredConstructor().newInstance();
}
2. 不能创建泛型数组
java
// 编译错误: 不能创建泛型数组
// List<String>[] array = new List<String>[10];
// 原因: 数组协变 + 类型擦除 = 类型不安全
Object[] objArray = new List[10]; // 合法
objArray[0] = new ArrayList<Integer>(); // 运行时不报错(擦除后都是List)
String s = ((List<String>) objArray[0]).get(0); // ClassCastException!
// 替代方案: 用 List<List<String>>
List<List<String>> lists = new ArrayList<>();
3. 不能用基本类型做类型参数
java
// List<int> list; // 编译错误
List<Integer> list = new ArrayList<>(); // 必须用包装类
// 自动装箱/拆箱开销
4. catch 泛型异常受限
java
// 不能 catch 泛型异常类型
// public <T extends Throwable> void demo() {
// try { } catch (T e) {} // 编译错误
// }
// 可以用类型通配
public <T extends Throwable> void demo(Class<T> type) throws T {
try {
// risky code
} catch (Throwable e) {
if (type.isInstance(e)) throw (T) e;
throw new RuntimeException(e);
}
}
5. 泛型类型不能用于 instanceof
java
List<String> list = new ArrayList<>();
// if (list instanceof List<String>) {} // 编译错误
if (list instanceof List<?>) {} // 合法
if (list instanceof List) {} // 合法(原始类型)
四、桥接方法
类型擦除会破坏多态,编译器通过桥接方法修复。
问题场景
java
// 源码
class Pair<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
class StringPair extends Pair<String> {
@Override
public void set(String value) { super.set(value); }
@Override
public String get() { return super.get(); }
}
java
// 擦除后
class Pair {
private Object value;
public void set(Object value) { this.value = value; } // 参数: Object
public Object get() { return value; } // 返回: Object
}
class StringPair extends Pair {
public void set(String value) { super.set(value); } // 参数: String
public String get() { return (String) super.get(); } // 返回: String
// 问题: set(String) 没有override父类的 set(Object)!
}
arduino
多态破坏:
Pair pair = new StringPair();
pair.set("hello");
// 编译器调用 pair.set(Object)
// 但 StringPair 只有 set(String)
// → 多态调用失败!
桥接方法修复
java
// 编译器自动生成的桥接方法 (在 StringPair 中)
// 方法签名: set(Object) --- 与父类匹配
public void set(Object value) { // 桥接方法
this.set((String) value); // 调用实际的 set(String)
}
// get 的桥接方法 (协变返回类型)
public Object get() { // 桥接方法
return this.get(); // 调用实际的 get() → String
}
vbnet
桥接方法解决多态:
Pair pair = new StringPair();
pair.set("hello");
编译器: pair.set(Object) → StringPair.set(Object)
↑ 这是桥接方法
→ 内部调用 set((String)"hello")
→ 实际执行 StringPair.set(String)
✓ 多态正确!
验证桥接方法
bash
# javap -c StringPair.class
public void set(java.lang.String);
Code:
aload_0
aload_1
invokespecial #2 // Method Pair.set:(Ljava/lang/Object;)V
return
public void set(java.lang.Object); // ← 桥接方法!
Code:
aload_0
aload_1
checkcast #3 // class java/lang/String
invokevirtual #4 // Method set:(Ljava/lang/String;)V
return
桥接方法上有一个 ACC_BRIDGE | ACC_SYNTHETIC 标志,表示编译器自动生成。
五、通配符与 PECS 原则
三种通配符
java
// 1. 无界通配符: 只读,不确定类型
void printAll(List<?> list) {
for (Object o : list) System.out.println(o);
// list.add("x"); // 编译错误: 不能写入
}
// 2. 上界通配符: ? extends T --- 生产者
void processNumbers(List<? extends Number> list) {
Number n = list.get(0); // 读: 安全,保证是 Number 子类
// list.add(42); // 编译错误: 不能写入
}
// 3. 下界通配符: ? super T --- 消费者
void addNumbers(List<? super Integer> list) {
list.add(42); // 写: 安全,Integer 可加入 super Integer 列表
// Number n = list.get(0); // 编译错误: 只能读到 Object
}
PECS 原则
Producer Extends, Consumer Super
r
┌────────────────────────────────────────────────────────┐
│ PECS 原则 │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Producer │ │ Consumer │ │
│ │ ? extends T│ │ ? super T │ │
│ │ │ │ │ │
│ │ 只读不写 │ │ 只写不读 │ │
│ │ 从中获取 │ │ 往里放入 │ │
│ │ ▲ │ │ ▼ │ │
│ │ 读取数据 │ │ 写入数据 │ │
│ └─────────────┘ └─────────────┘ │
│ │
│ 如果既要读又要写 → 不用通配符,用 <T> │
└────────────────────────────────────────────────────────┘
PECS 实战
java
// 场景: 从源集合拷贝到目标集合
public static <T> void copy(
List<? super T> dest, // 消费者 (写入)
List<? extends T> src) { // 生产者 (读取)
for (int i = 0; i < src.size(); i++) {
dest.set(i, src.get(i));
}
}
// 实际例子
List<Number> numbers = new ArrayList<>(Arrays.asList(1, 2, 3));
List<Integer> integers = Arrays.asList(4, 5, 6);
copy(numbers, integers); // Integer → Number,OK
List<Object> objects = new ArrayList<>();
copy(objects, integers); // Integer → Object,OK
Collections.copy 源码验证
java
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");
// ...
for (int i = 0; i < srcSize; i++)
dest.set(i, src.get(i));
}
六、泛型方法 vs 泛型类
java
// 泛型类: 在类级别声明
public class Box<T> {
private T item;
public void put(T item) { this.item = item; }
}
// 泛型方法: 在方法级别声明,<T> 放在返回类型前
public class Utils {
public static <T> T first(List<T> list) {
return list.isEmpty() ? null : list.get(0);
}
// 多类型参数
public static <K, V> Map<K, V> zip(K[] keys, V[] values) {
Map<K, V> map = new HashMap<>();
for (int i = 0; i < keys.length; i++)
map.put(keys[i], values[i]);
return map;
}
// 有界类型参数
public static <T extends Comparable<T>> T max(List<T> list) {
T result = list.get(0);
for (T item : list) {
if (item.compareTo(result) > 0)
result = item;
}
return result;
}
}
七、泛型与继承
java
// 泛型不协变!
// List<String> 不是 List<Object> 的子类
List<String> strings = new ArrayList<>();
// List<Object> objects = strings; // 编译错误!
// 原因: 如果允许,objects.add(42) 会破坏类型安全
// 数组是协变的 (设计缺陷)
Object[] objArr = new String[10];
objArr[0] = 42; // ArrayStoreException! 运行时错误 (非编译期)
// 泛型通过通配符实现安全协变
List<? extends Object> list = new ArrayList<String>(); // OK
ini
协变对比:
┌──────────────────────────────────────────────────┐
│ 数组 (协变): │
│ String[] sa = new String[10]; │
│ Object[] oa = sa; ← 合法 (协变) │
│ oa[0] = 42; ← 运行时异常 │
│ 问题: 类型检查推迟到运行时 │
│ │
│ 泛型 (不协变): │
│ List<String> ls = new ArrayList<>(); │
│ List<Object> lo = ls; ← 编译错误 │
│ 优势: 类型安全在编译期保证 │
│ │
│ 通配符 (安全协变): │
│ List<? extends Object> lo = ls; ← 合法 │
│ lo.add("x"); ← 编译错误(只读) │
│ 安全: 编译期阻止不安全操作 │
└──────────────────────────────────────────────────┘
八、泛型类型推断演进
java
// Java 5: 泛型引入
Map<String, List<Integer>> map = new HashMap<String, List<Integer>>();
// Java 7: 菱形操作符 <>
Map<String, List<Integer>> map = new HashMap<>();
// Java 8: 方法上下文推断
List<String> list = Collections.emptyList(); // 自动推断 T=String
// Java 8: lambda 参数推断
list.stream().filter(s -> s.length() > 3).collect(Collectors.toList());
// Java 10+: var 局部变量类型推断
var map = new HashMap<String, List<Integer>>();
九、面试高频问题速答
Q1: 为什么 Java 选择类型擦除而不是像 C++ 那样保留泛型类型?
兼容性。Java 5 引入泛型时,已有大量非泛型代码。类型擦除让泛型代码和非泛型代码可以互操作。代价是运行时丢失泛型信息(不像 C# 的 reified generics,会保留类型信息)。
Q2: 类型擦除后怎么获取泛型信息?
通过反射可以获取部分泛型信息:Method.getGenericReturnType()、Field.getGenericType() 可以拿到 ParameterizedType,因为类元数据(Class 文件的 Signature 属性)记录了泛型签名。但对象的 getClass() 拿不到,因为擦除后运行时类型相同。
Q3: 桥接方法是什么?
类型擦除后,子类override方法的签名可能与父类不一致(如父类 set(Object),子类 set(String))。编译器自动在子类生成一个签名匹配父类的桥接方法,内部调用实际方法。桥接方法有 ACC_BRIDGE | ACC_SYNTHETIC 标志。
Q4: PECS 原则怎么应用?
- 需要从集合读取 数据:
List<? extends T>(生产者用 extends) - 需要向集合写入 数据:
List<? super T>(消费者用 super) - 既读又写:
List<T>(不用通配符) - Collections.copy 就是经典 PECS 应用
Q5: List<?> 和 List<Object> 有什么区别?
List<?> 是无界通配符,只读不能写(除了 null)。List<Object> 可以读写任何 Object。List<?> 比 List<Object> 更安全,强制你只读。
Q6: 泛型方法 public <T> T getById(Class<T> clazz) 和泛型类有什么区别?
泛型类的类型参数在实例化时确定,整个实例共享。泛型方法的类型参数在方法调用时确定,每次调用可以不同。泛型方法可以独立于类是否泛型化。
十、总结
javascript
Java 泛型核心知识:
┌──────────────────────────────────────────────────┐
│ │
│ 类型擦除: │
│ 编译后 T → Object / 上界类型 │
│ 运行时所有泛型实例共享同一个 Class │
│ 编译器在读取处自动插入 checkcast │
│ │
│ 桥接方法: │
│ 类型擦除破坏多态 → 编译器生成桥接方法修复 │
│ 签名: 与父类匹配 (Object类型) │
│ 内部: cast + 调用实际方法 │
│ 标志: ACC_BRIDGE | ACC_SYNTHETIC │
│ │
│ PECS 原则: │
│ 生产者 (读): ? extends T │
│ 消费者 (写): ? super T │
│ 读写都有: <T> │
│ │
│ 限制: │
│ 不能: new T()、T.class、T[] │
│ 不能: instanceof List<String> │
│ 不能: catch T │
│ 不能: 静态字段用类型参数 │
│ 不能: 基本类型做类型参数 (用包装类) │
│ │
│ 设计哲学: 编译期类型安全 + 运行时兼容性 │
│ (换取了运行时类型信息丢失) │
│ │
└──────────────────────────────────────────────────┘
Java 的泛型设计是工程妥协的产物------用类型擦除换来了向后兼容,代价是运行时类型信息丢失和一系列使用限制。理解类型擦除、桥接方法和 PECS 原则,不仅面试能答到位,日常编码中也能避免泛型陷阱。