我们都知道 Java 中的泛型可以在编译期对类型检查,避免类型强制转化带来的问题,保证代码的健壮性。不同语言对泛型的支持也不一样,Java 中的泛型类型在编译期会擦除,下面一个例子可以证明这一点:
public static void main(String[] args) throws Exception {
List<String> list = new ArrayList<>();
list.add("abc");
Class<? extends List> listClass = list.getClass();
Method addMethod = listClass.getMethod("add", Object.class);
addMethod.invoke(list, 10);
System.out.println(list.size());
}
上面这个例子中定义了一个 String
类型的集合,通过反射获取 add
方法并调用该方法添加了一个 int
类型的元素,代码执行过程中并不会出现异常,而且能正常输出 size = 2。
因为泛型会被擦除,所以很多人都认为在代码运行期间是无法得知泛型参数类型的。最近重温 Java 基础语法的时候,Java Reflection - Generics 在文章开头认为这种结论不是很准确,因为通过反射可以获取到泛型的具体类型,就像下面这样:
public class GenericsSamples {
public static List<String> getStringList() {
return new ArrayList<>();
}
public static void main(String[] args) throws NoSuchMethodException {
Method method = GenericsSamples.class.getMethod("getStringList", null);
Type returnType = method.getGenericReturnType();
if (returnType instanceof ParameterizedType) {
ParameterizedType type = (ParameterizedType) returnType;
Type[] typeArguments = type.getActualTypeArguments();
for (Type typeArgument : typeArguments) {
Class<?> typeArgClass = (Class<?>) typeArgument;
System.out.println("typeArgClass = " + typeArgClass);
}
}
}
}
typeArgClass
输出的类型为 class java.lang.String
,为什么编译期间被擦除的泛型,在代码运行期间能获取到呢?原因是为了让虚拟机解析泛型类型,在虚拟机规范里引入了 LocalVariableTypeTable
属性,我们可以通过 javap -v class
反编译方式查看。
public class GenericsDemo {
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();
System.out.println(stringList.getClass().equals(integerList.getClass()));
}
}
反编译上面 GenericsDemo
的 class 文件,main 方法编译信息如下:
从上面的编译信息可以看出属性 LocalVariableTypeTable
的 Signature
中保存了泛型的类型信息,因此我们可以通过反射在运行阶段获取到它。到这里可能会有人有个疑问:泛型不是被擦除了吗,为啥还能保存到 Signature
中,原因是它和我们所说的泛型擦除是两码事,下面再来看一个例子:
public class GenericSamples<T> {
private T param;
public T getParam() { return param; }
public void setParam(T param) { this.param = param; }
public static void main(String[] args) {
GenericSamples<Integer> integerGenericSamples = new GenericSamples<>();
integerGenericSamples.setParam(10);
System.out.println(integerGenericSamples.getParam());
}
}
下面是 get 与 set 方法反编译的结果:
set 与 get 方法通过反编译后并不会保留类型信息,统一处理成了 Object
,这个才是我们通常所说的泛型擦除,由于被编译器当作 Object
类型处理,那么我们就可以通过反射 set 任意类型的参数。
和 LocalVariableTypeTable
类似的还有一个 LocalVariableTable
,不同的地方在于前者的 Signature
相较于后者的 descriptor
保存了泛型信息,关于这两个属性更多的介绍可以查看官方的介绍:4.7.13. The LocalVariableTable Attribute。
参考: