Java 泛型的类型擦除与反射获取
立泉Java 泛型于 2004 年 J2SE 5.0 发布时作为核心语言特性出现,目的是将一些运行时的类型转换异常提前到编译期发现。
// 泛型出现之前
List list = new ArrayList();
list.add("text");
// 允许放入非预期类型,add 方法的参数类型是 Object,编译通过
list.add(Integer.valueOf(42));
for (int i = 0; i < list.size(); i++) {
// 运行时抛出 ClassCastException 类型转换异常
String s = (String) list.get(i);
}
// 泛型出现之后,创建容器对象时指定所存储数据的类型
List<String> list = new ArrayList<String>();
list.add("text");
// 不允许放入与容器参数化类型不匹配的对象,编译时报错
list.add(Integer.valueOf(42));
具有 Parameterized Types 参数化类型的类或接口称为 Generic Type 泛型:
// 泛型声明,类似于函数声明,尖括号中是泛型的参数化类型,使用泛型时需指定类型实参
class Holder<T> {
private T data;
public Holder(T data) {
this.data = data;
}
public T getData() {
return this.data;
}
}
// 使用泛型,传入类型实参
Holder<String> holder = new Holder<String>("Data");
// 编译期即能够检查类型是否匹配
String data = holder.getData();
不同于 C++ 模版类编译时将泛型与其参数化类型组合为独立的新类型,Java 泛型是通过类型擦除实现,即编译时参数化类型会被擦除到其声明时的上边界,默认是Object,这样能够向后兼容既有的类库和字节码,而且能避免类型数量膨胀导致内存占用显著增加。
// C++ 模版类,声明泛型的参数化类型 T
// 编译器以此为模版生成泛型与其参数化类型组合的新类型
template <typename T>
class Holder {
private:
T data;
public:
explicit Holder(T value) : data(std::move(value)) {}
const T& getData() const noexcept {
return data;
}
T& getData() noexcept {
return data;
}
};
// 编译后是两种独立的类型
Holder<string> holder("string");
Holder<int> holder(10);
Java 的class Holder<T>编译后参数化类型被擦除到上边界Object,Holder<String>和Holder<Integer>编译产物生成的是同一个Holder.class字节码文件,通过getClass()获得的是该类型的同一个实例对象。
Holder<String> strHolder = new Holder("string");
Holder<Integer> intHolder = new Holder(10);
// 输出为 true
System.out.println(strHolder.getClass() == intHolder.getClass());
正因为Holder<String>和Holder<Integer>编译后是同一个Holder.class,运行时不能区分obj instanceof Holder<String>和obj instanceof Holder<Integer>,所以编译器直接禁止这么做,只允许使用通配符obj instanceof Holder<?>。
协变
Java 的数组具有协变性,即若Fruit是Apple的父类型,则其数组Fruit[]亦是Apple[]的父类型,这种协变性极易在运行时出现转型异常:
// fruits 实际是一个 Apple 数组,向上转型为 Fruit[]
Fruit[] fruits = new Apple[10];
// 添加 Apple 没问题
fruits[0] = new Apple();
// 添加 Fruit 的另一个子类对象可以通过编译,但运行时抛出转型异常,Banana 不能转型为 Apple
fruits[1] = new Banana();
泛型的初衷是将运行时的类型检查提前到编译期并确保通过编译的代码不会出现类型转换异常,任何与这一原则冲突的特性都被编译器禁止,所以泛型不具有协变性,Holder<Fruit>不是Holder<Apple>的父类型。只能通过Holder<? extends Fruit>支持协变,但为避免运行时转型异常,编译器禁止向该容器中添加新元素。
class Holder<T> {
private T data;
public void addData(T data) {
this.data = data;
}
public T getData() {
return data;
}
}
// 通过 ? extends 支持泛型协变
Holder<? extends Fruit> holder = new Holder<Apple>();
// 这里编译不通过,Holder<? extends Fruit> 表示内部存储的参数化类型是 Fruit 的某个子类型
// 不一定是 Apple,运行时可能出现转型异常,所以编译器禁止这种操作
holder.addData(new Apple());
// 而向上转型是安全的,Fruit 的子类型可以转型为 Fruit
Fruit fruit = holder.getData();
类似Holder<? extends Fruit>,还有另一种定义泛型实参的通配符写法Holder<? super Fruit>,它不具备协变性。
// 编译不通过,因为会出现与数组协变同样的运行时转型不安全问题,编译器禁止这么做
Holder<? super Fruit> holder = new Holder<Apple>();
// 这样是可以的,holder 中存储的是 Fruit 的某个父类型
Holder<? super Fruit> holder = new Holder<Fruit>();
// 向上转型是安全的
holder.addData(new Apple())
// 为避免可能的转型异常,编译器只允许以 Object 的方式获取内部的参数化类型实例
// 向 Object 转型是安全的
Object obj = holder.getData();
反射
类型擦除指的是编译时将泛型的参数化类型替换为具体的上边界类型,但类声明、接口声明、类继承关系以及方法签名中的泛型声明信息并没有消失,而是被写入对应class文件的元数据 Signature 属性中,可以通过 Java 的反射机制获取完整实参。
// 直接创建泛型实例,结果是 java.util.AbstractList<E>
// 因为 AbstractList 的泛型声明是 class AbstractList<T>,所以通过反射获取到的是形参 T
System.out.println(new ArrayList<Integer>().getClass().getGenericSuperclass());
// 继承结构的基类泛型声明,类型形参 T
class Holder<T> {
...
}
// 子类继承泛型基类,传入类型实参 String
// 编译器在生成 StringHolder.class 时,会将其父类明确记录为 Holder<Ljava/lang/String;>,而不是裸类型 Holder
class StringHolder extend Holder<String> {
...
}
// 获取带泛型信息的基类类型,因为基类是泛型,所以返回 ParameterizedType
Type type = StringHolder.class.getGenericSuperclass();
if (type instanceof ParameterizedType pt) {
// 获取泛型实参
Type[] typeArgs = pt.getActualTypeArguments();
Class<?> classStr = (Class<?>) typeArgs[0];
// 输出 class java.lang.String
System.out.println(classStr);
}
// 创建泛型类的匿名子类对象,会生成独立的 class 文件,将其父类明确记录为 ArrayList<Integer>
// 结果是 java.util.ArrayList<java.lang.Integer>,获得到正确的泛型参数类型
System.out.println(new ArrayList<Integer>(){}.getClass().getGenericSuperclass());
通过创建泛型基类的匿名子类对象将动态传入的泛型实参固定到class文件元信息中具有非常大的灵活性,Gson等一些反序列化类库就是通过这一机制动态生成指定类型的对象:
class GenericsToken<T> {
private Type type;
public GenericsToken() {
Type superClass = this.getClass().getGenericSuperclass();
type = ((ParameterizedType) superClass).getActualTypeArguments()[0];
}
public Type getType() {
return type;
}
}
class Man {
String name;
public Man(String name) {
this.name = name;
}
@Override
public String toString() {
return "Man name is " + name;
}
}
// 创建泛型基类的匿名子类对象,将泛型实参固定到 class 文件的元信息中,运行时通过反射获取
Type classType = new GenericsToken<Man>(){}.type;
try {
// 通过反射创建泛型实参类型的实例对象
Man man = (Man) ((Class)classType).getDeclaredConstructor(String.class).newInstance("Tom");
System.out.println(man);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}