擦拭法
约 1359 字大约 5 分钟
2025-07-17
4.1 擦拭法的定义
泛型是一种 " 模板代码 " 技术,不同语言有不同的实现方式。 Java 使用的是擦拭法 (Type Erasure)。
擦拭法是指虚拟机对泛型一无所知,所有工作都在编译期完成。 编译器会将泛型类型擦除为它的原始类型,通常是 Object
。
4.2 编译器与虚拟机的视角
编译器看到的是带有泛型参数的代码,例如 Pair<T>
。
public class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
}
虚拟机执行的代码不包含泛型信息。 泛型类型 T
会被替换为 Object
。
public class Pair {
private Object first;
private Object last;
public Pair(Object first, Object last) {
this.first = first;
this.last = last;
}
public Object getFirst() {
return first;
}
public Object getLast() {
return last;
}
}
结论: Java 使用擦拭法实现泛型,编译器将类型 <T>
视为 Object
,并根据 <T>
实现安全的强制转型。
4.3 擦拭法的具体过程
当使用泛型时,编译器会进行类型检查,并在需要时插入强制类型转换。
Pair<String> p = new Pair<>("Hello", "world");
String first = p.getFirst();
String last = p.getLast();
Pair p = new Pair("Hello", "world");
String first = (String) p.getFirst();
String last = (String) p.getLast();
4.4 擦拭法的局限性
由于 Java 泛型采用擦拭法实现,存在以下局限性:
4.4.1 局限一:类型 <T>
不能是基本类型
因为实际类型是 Object
,而 Object
类型无法持有基本类型。
Pair<int> p = new Pair<>(1, 2); // compile error!
4.4.2 局限二:无法取得带泛型的 Class
对于 Pair<String>
和 Pair<Integer>
类型,getClass()
返回的是同一个 Class
实例,即 Pair.class
。
public class Main {
public static void main(String[] args) {
Pair<String> p1 = new Pair<>("Hello", "world");
Pair<Integer> p2 = new Pair<>(123, 456);
Class c1 = p1.getClass();
Class c2 = p2.getClass();
System.out.println(c1==c2); // true
System.out.println(c1==Pair.class); // true
}
}
class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
}
因此,所有泛型实例,无论 T
的类型是什么,getClass()
返回同一个 Class
实例,因为编译后它们全部都是 Pair<Object>
。
4.4.3 局限三:无法判断带泛型的类型
无法使用 instanceof
判断带泛型的类型。
Pair<Integer> p = new Pair<>(123, 456);
// Compile error:
if (p instanceof Pair<String>) {
}
原因和前面一样,并不存在 Pair<String>.class
,而是只有唯一的 Pair.class
。
4.4.4 局限四:不能实例化 T
类型
不能直接使用 new T()
实例化泛型类型,因为擦拭后会变成 new Object()
。 必须借助额外的 Class<T>
参数。
public class Pair<T> {
private T first;
private T last;
public Pair(Class<T> clazz) {
try {
first = clazz.getDeclaredConstructor().newInstance();
last = clazz.getDeclaredConstructor().newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
使用示例:
Pair<String> pair = new Pair<>(String.class);
4.5 不恰当的覆写方法
定义的方法签名如果和 Object
类的方法签名相同,可能会导致编译错误。 例如,equals(T t)
会被擦拭成 equals(Object t)
,与 Object.equals(Object)
冲突。 可以通过更换方法名来避免冲突。
public class Pair<T> {
public boolean same(T t) {
return this == t;
}
}
4.6 泛型继承
一个类可以继承自一个泛型类。例如,如果父类的类型是 Pair<Integer>
,子类的类型是 IntPair
,可以采用如下方式继承:
public class IntPair extends Pair<Integer> {
}
在使用时,因为子类 IntPair
没有泛型类型,所以可以像普通类一样使用:
IntPair ip = new IntPair(1, 2);
如前所述,我们通常无法直接获取 Pair<T>
的 T
类型。但是,当父类是泛型类型时,编译器必须将类型 T
(对于 IntPair
来说,也就是 Integer
类型) 保存到子类的 class 文件中。这是因为编译器需要确保 IntPair
只能存储和操作 Integer
类型的对象。
在继承了泛型类型的情况下,子类可以获取父类的泛型类型。例如,IntPair
可以获取到父类的泛型类型 Integer
。获取父类的泛型类型代码如下:
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
public class Main {
public static void main(String[] args) {
Class<IntPair> clazz = IntPair.class;
Type t = clazz.getGenericSuperclass();
if (t instanceof ParameterizedType) {
ParameterizedType pt = (ParameterizedType) t;
Type[] types = pt.getActualTypeArguments(); // 可能有多个泛型类型
Type firstType = types[0]; // 取第一个泛型类型
Class<?> typeClass = (Class<?>) firstType;
System.out.println(typeClass); // Integer
}
}
}
class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
}
class IntPair extends Pair<Integer> {
public IntPair(Integer first, Integer last) {
super(first, last);
}
}
代码解释:
- 通过
IntPair.class
获取IntPair
类的Class
对象。 - 使用
getGenericSuperclass()
方法获取其泛型父类的Type
对象。如果该Type
对象是ParameterizedType
的实例,则表示它是一个参数化类型,即泛型类型。 - 通过
getActualTypeArguments()
方法可以获取到泛型类型参数的数组,进而可以获取到具体的泛型类型。
这个特性允许我们在运行时检查和使用泛型类型信息,这在某些框架和库的实现中非常有用,例如,依赖注入、对象关系映射 (ORM) 等。
4.7 类型系统结构
Java 的类型系统在引入泛型后变得更加复杂,Class
类已经无法完全表示所有类型。实际上,Java 的类型系统主要由 Type
接口以及它的四个子接口组成,分别是 Class
、ParameterizedType
、GenericArrayType
和 WildcardType
。
┌────┐
│Type│
└────┘
▲
│
┌────────────┬────────┴─────────┬───────────────┐
│ │ │ │
┌─────┐┌─────────────────┐┌────────────────┐┌────────────┐
│Class││ParameterizedType││GenericArrayType││WildcardType│
└─────┘└─────────────────┘└────────────────┘└────────────┘