1. 概述
本文将深入探讨 Java 泛型中的一个核心机制——类型擦除(Type Erasure)。这个机制虽然常被开发者忽略,但理解它对写出健壮的泛型代码至关重要。
2. 什么是类型擦除?
类型擦除的本质是:只在编译时检查类型约束,运行时丢弃所有类型信息。
简单来说,编译器会在编译阶段:
- 验证类型安全性
- 插入必要的类型转换
- 然后彻底擦除泛型类型信息
来看个例子:
public static <E> boolean containsElement(E[] elements, E element) {
for (E e : elements) {
if (e.equals(element)) {
return true;
}
}
return false;
}
编译后实际变成这样:
public static boolean containsElement(Object[] elements, Object element) {
for (Object e : elements) {
if (e.equals(element)) {
return true;
}
}
return false;
}
所有泛型类型 E
都被替换为 Object
,这就是类型擦除的直观体现。
3. 类型擦除的分类
类型擦除主要发生在两个层面:类级别和方法级别。
3.1. 类级别类型擦除
编译器会擦除类上的类型参数,规则如下:
- 有边界(bound)时,替换为第一个边界类型
- 无边界时,替换为
Object
用 Stack
示例说明:
public class Stack<E> {
private E[] stackContent;
public Stack(int capacity) {
this.stackContent = (E[]) new Object[capacity];
}
public void push(E data) {
// ..
}
public E pop() {
// ..
}
}
编译后变成:
public class Stack {
private Object[] stackContent;
public Stack(int capacity) {
this.stackContent = (Object[]) new Object[capacity];
}
public void push(Object data) {
// ..
}
public Object pop() {
// ..
}
}
如果类型参数有边界:
public class BoundStack<E extends Comparable<E>> {
private E[] stackContent;
public BoundStack(int capacity) {
this.stackContent = (E[]) new Object[capacity];
}
public void push(E data) {
// ..
}
public E pop() {
// ..
}
}
编译后 E
被替换为第一个边界 Comparable
:
public class BoundStack {
private Comparable[] stackContent;
public BoundStack(int capacity) {
this.stackContent = (Comparable[]) new Object[capacity];
}
public void push(Comparable data) {
// ..
}
public Comparable pop() {
// ..
}
}
3.2. 方法级别类型擦除
方法上的类型参数同样会被擦除:
- 无边界 → 替换为
Object
- 有边界 → 替换为第一个边界类型
打印数组的方法示例:
public static <E> void printArray(E[] array) {
for (E element : array) {
System.out.printf("%s ", element);
}
}
编译后:
public static void printArray(Object[] array) {
for (Object element : array) {
System.out.printf("%s ", element);
}
}
有边界的情况:
public static <E extends Comparable<E>> void printArray(E[] array) {
for (E element : array) {
System.out.printf("%s ", element);
}
}
编译后:
public static void printArray(Comparable[] array) {
for (Comparable element : array) {
System.out.printf("%s ", element);
}
}
4. 边界情况与陷阱
类型擦除有时会导致一些"坑",特别是涉及继承和方法重写时。
4.1. 桥接方法(Bridge Methods)
当子类继承泛型父类时,编译器可能生成桥接方法来保持多态性。
看这个例子:
public class IntegerStack extends Stack<Integer> {
public IntegerStack(int capacity) {
super(capacity);
}
public void push(Integer value) {
super.push(value);
}
}
现在执行这段代码:
IntegerStack integerStack = new IntegerStack(5);
Stack stack = integerStack; // 注意这里用原始类型
stack.push("Hello"); // 竟然能编译通过!
Integer data = integerStack.pop(); // 运行时抛 ClassCastException
类型擦除后,Stack.push()
变成 push(Object)
,所以 stack.push("Hello")
能通过编译。但在运行时:
IntegerStack
继承了push(Object)
方法- 实际调用的是父类方法,导致
String
被塞入IntegerStack
pop()
时类型转换失败
✅ 解决方案:编译器自动生成桥接方法:
public class IntegerStack extends Stack {
// 编译器生成的桥接方法
public void push(Object value) {
push((Integer) value); // 转型为 Integer
}
// 原始方法
public void push(Integer value) {
super.push(value);
}
}
这样当调用 push(Object)
时,会通过桥接方法正确调用 push(Integer)
,保证类型安全。
⚠️ 关键点:
- 桥接方法是编译器自动生成的合成方法
- 它们确保泛型类型擦除后仍能保持多态性
- 在字节码中可见,但源码中看不到
5. 总结
类型擦除是 Java 泛型的核心机制,总结要点:
✅ 核心原则:
- 泛型只在编译期有效
- 运行时所有类型参数被擦除为
Object
或边界类型 - 编译器自动插入类型转换保证安全
✅ 实际影响:
- 泛型类不能使用基本类型(如
List<int>
不合法) - 不能实例化泛型类型(如
new E()
) - 运行时无法获取具体类型(如
List<String>.class
不存在)
❌ 常见误区:
- 误以为运行时保留泛型信息(可通过反射部分获取)
- 误以为泛型数组是类型安全的(实际需要特殊处理)
深入理解类型擦除能帮你:
- 避免泛型相关的运行时错误
- 正确使用泛型 API 设计
- 理解框架(如 Spring、Jackson)中的泛型处理机制
延伸阅读: