擦拭法
约 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│
└─────┘└─────────────────┘└────────────────┘└────────────┘