深入解析Java泛型与反射:类型擦除、匿名类实例生成及运行时类型获取技巧
阿里巴巴简介

本文从Java泛型开始,研究反射对泛型的扩展以及类型擦除的影响。然后通过生成匿名类实例的小技巧,我们获得了泛型运行时类型的技能。 (后台回复【Java单元测试实践】即可获取电子书)
介绍
如果你经常写工具类,很可能会遇到一个实际问题,就是需要获取泛型表达式A中T的运行时类型。获取它需要一些技巧。但很少有人彻底解释这项技术为何有效。在下一篇文章中,我们将从Java泛型(Generics)开始,结合JLS(Java Language Standard,Java语言规范)和JVMS(Java Virtual Machine Standard,Java虚拟机规范),并使用javac编译过程来生成源代码泛型处理的思路,结合对JRE反射API源码的探索,最终用虚拟机实现(OpenJDK8的Hotspot)进行了验证,从根本上回答了这个问题。
介绍
在编写工具类或者使用通用简化逻辑时,往往需要获取运行时的类型信息来进行下一步的逻辑判断,比如业务中经常使用的插件架构,或者反序列化时获取类型信息。细节。例如,应该有很多新手会尝试编写下面的代码,通过反射和泛型来获取某个类型的运行时类型。
public staticClass typeOf(T obj) {return (Class)obj.getClass(); }是的。上面的代码在一定范围内可以很好的工作。然而,遇到泛型时,却适得其反。可以说,自从Java 1.5引入泛型以来,反射中泛型的处理一直是一件麻烦的事情。
泛型
泛型是Java 1.5中引入的一个特性,主要是为了解决类型检查(Type Checking)问题[1],为我们在编写通用代码(尤其是库)时提供强类型约束,而不必担心1.5之前天上的随机铸造造成堆污染[13]。
为什么我们要引入泛型?关于泛型设计的思想,可以参考1998年OOPLSA会议上Gilad Bracha的论文[2]。后来形成了JSR14:Adding Generics to the Java Programming Language
在java[14]中添加泛型,最终在1.5进入JDK。
泛型的正式定义请参考JLS[3],[4],[5],[6]
泛型的非正式定义可以参考下面的简单代码。例如,几个容易混淆的概念,类型变量,类型参数和类型参数也进行了解释。
/*** 定义一个泛型类,其中** Type Parameter是T extends Number* Type Variable是T* Type Argument是Foo里的Integer */class Foo<T extends Number> {}反射
由于泛型是在 1.5 中引入的,因此反射也针对新概念进行了相应的扩展。
在实现上,反射引入了Type接口,以及派生的接口和类,实现了通用JLS的标准。它们的UML类型如下。
其中,我们最需要处理的是ParameterizedType[10]
ParameterizedType可能是一个比较陌生的概念。不过,经常使用 Core Reflection API 的开发者可能会比较熟悉。 Type 类型的派生类是 ParameterizedType。这个概念的非正式(白话)解释可以简单地类比为泛型类型 Foo 的实现。例如,Foo 和 Foo 分别是 Foo 的 ParameterizedType。
同时,随着泛型的实现,反射API中添加了一系列名称中带有Generic的方法和类[8]。这些是我们后面获取泛型运行时类型的基础。
类型擦除
虽然Java引入了泛型的概念,但为了保持前向兼容性(JVM层面的兼容,不需要对Bytecode和JVM设计进行任何改变),以及编译性能(相比C++的Template,用模板参数生成新类型), Java 类型擦除[9]作为泛型的实现解决方案被引入。通过类型擦除,Java不需要对虚拟机实现进行任何修改,也不会为ParameterizedType创建新的类。
类型擦除陷阱
与所有设计一样,都需要权衡。 Java 泛型获得了类型擦除的好处,但也导致了两个主要问题。
Java泛型是编译时的,这意味着所有泛型信息在运行时都会被删除。所以JVM无法感知到类型的存在。
因此,我们无法在运行时通过反射API获取Type Variable所表示的类型。
但这个特性导致我们在编写工具类时遇到一些困难。例如,不能单独通过T创建实例。如果 T 是非泛型类也没关系。我们可以通过直接传入类型信息来进行一些操作。
public static finalvoid foo(List list, Class<T> tClass) 但当T为ParameterizedType时,上述接口中的tClass类型信息只能获取ParameterizedType中的非泛型类型信息。例如,当T在List中时,Class是List.class。在某些场景下,比如反序列化,你会遇到一些麻烦。
获取泛型运行时类型的提示
引入类型参考
那么,在Java中,有没有办法获得通用的运行时类型呢?答案是肯定的。但我们需要做出一些改变。例如,我们可以在很多序列化框架(如Jackson、Fastjson)中看到TypeReference或类似的设计机制。在“基本上”不改变函数签名的情况下获取 T 的运行时类型。
我们定义类
class Wrapper{ }它非常简单,基本上就像一个包装类。然后做一个简单的方法定义
public staticType getGenericRuntimeType(Wrapper wrapper )最后还有一个小技巧,就是创建一个匿名派生类的实例,配合反射API首先获取superClass的泛型信息。如果是ParameterizedType,则尝试获取真实的Type Argument信息,即可获取T的运行时类型。
public staticType getGenericRuntimeType(Wrapper wrapper) { Type type = wrapper.getClass().getGenericSuperclass();if (type == null) {return null;}if (type instanceof ParameterizedType) {Type[] types = ((ParameterizedType)type).getActualTypeArguments();return types[0];}return null;}例如,以下两条语句之间的唯一区别是第 2 行创建了一个匿名类 Wrapper。
Type type1 = getGenericRuntimeType(new WrapperString
>>());Type type2 = getGenericRuntimeType(new WrapperString
>>() {});不过运行后的最终结果是单独打印的。
nulljava.util.List<java.lang.String>那么为什么仅仅一个匿名类的实例就能产生如此巨大的差异呢?泛型类型的获取能否在类型擦除的框架下完成?理由是什么?
事实上,它使用了JSR14[14]中的一种技术,将类中的通用信息保存到类签名(Signature)中。
类文件需要以向后兼容的方式携带通用类型信息。这是通过为类、方法和字段引入新的“Signature”属性来实现的。
首先,Java编译器将通用信息写入ClassFile的Signature属性中。然后通过JRE的反射接口解析Signature中的字符串。最后,隐藏的运行时类型信息被“挑出来”。接下来我们从JVMS的定义开始,研究Java代码的编译和生成ClassFile的过程,并找出JRE反射代码。
原理分析
JVM 类文件标准
JVM的ClassFile是Java源文件编译后生成的二进制格式。类似于Linux下的ELF或者Windows下的COFF,可以简单理解为JVM的“可执行文件”。 JVM读取它,执行字节码,最终执行程序。 ClassFile的格式如下:
ClassFile {u4 magic;u2 minor_version;u2 major_version;u2 constant_pool_count;cp_info constant_pool[constant_pool_count-1];u2 access_flags;u2 this_class;u2 super_class;u2 interfaces_count;u2 interfaces[interfaces_count];u2 fields_count;field_info fields[fields_count];u2 methods_count;method_info methods[methods_count];u2 attributes_count;attribute_info attributes[attributes_count];}属性数组是存储通用信息的地方,如 JSR14 中所述。 JVMS指出[11]
Java 编译器必须为其声明使用类型变量或参数化类型的任何类、接口、构造函数、方法或字段发出签名
可以看到Java编译器需要将泛型类信息带入Signature属性中,然后存储在编译后的ClassFile中。
做一个小实验
我们简单继承Wrapper类,编译后通过javap验证一下,得出结论。
public class ExtendedWrapper extends Wrapper> {
}javap后可以看到42行类的Signature有对应的类型信息(Lcom/aliyun/cwz/model/Wrapper;>;)。基本验证了JVMS标准
Classfile /Users/alibaba/myprojects/GenericsAndReflection/target/test-classes/com/aliyun/cwz/impl/ExtendedWrapper.classLast modified 2023-4-17; size 413 bytesMD5 checksum 96ca23aed30b94c2a445bbd76189e250Compiled from "ExtendedWrapper.java"public class com.aliyun.cwz.impl.ExtendedWrapper extends com.aliyun.cwz.model.Wrapper> minor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPERConstant pool:#1 = Methodref #3.#15 // com/aliyun/cwz/model/Wrapper."":()V #2 = Class #16 // com/aliyun/cwz/impl/ExtendedWrapper#3 = Class #17 // com/aliyun/cwz/model/Wrapper#4 = Utf8#5 = Utf8 ()V#6 = Utf8 Code#7 = Utf8 LineNumberTable#8 = Utf8 LocalVariableTable#9 = Utf8 this#10 = Utf8 Lcom/aliyun/cwz/impl/ExtendedWrapper;#11 = Utf8 Signature#12 = Utf8 Lcom/aliyun/cwz/model/Wrapper;>; #13 = Utf8 SourceFile#14 = Utf8 ExtendedWrapper.java#15 = NameAndType #4:#5 // "":()V #16 = Utf8 com/aliyun/cwz/impl/ExtendedWrapper#17 = Utf8 com/aliyun/cwz/model/Wrapper{public com.aliyun.cwz.impl.ExtendedWrapper();descriptor: ()Vflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method com/aliyun/cwz/model/Wrapper."":()V 4: returnLineNumberTable:line 7: 0LocalVariableTable:Start Length Slot Name Signature0 5 0 this Lcom/aliyun/cwz/impl/ExtendedWrapper;}Signature: #12 // Lcom/aliyun/cwz/model/Wrapper;>; SourceFile: "ExtendedWrapper.java"那么Java编译器是如何运行的呢?
探索Java编译器
根据《The Hitchhiker's Guide to javac》[15],JavaCompiler是javac的驱动程序。因此,我们可以通过研究JavaCompiler的实现来研究javac编译文件的过程(通过strace javac的编译过程,可以看到调用中包含了JavaCompiler,基本上可以得到同样的结论)。
我们使用OpenJDK1.8来看看JavaCompiler是如何编译ExtendWrapper的。
首先,JavaCompiler类在编译过程中调用compile方法。这是核心的编译方法,最终会输出Java类作为ClassFile。
com.sun.tools.javac.main.JavaCompiler#compile(com.sun.tools.javac.util.List<javax.tools.JavaFileObject>, com.sun.tools.javac.util.List<java.lang.String>, java.lang.Iterable extends javax.annotation.processing.Processor>)在这个函数中,Java文件首先会被解析器解析成JCTree.JCCompilationUnit。它是抽象语法树中的基本单元。然后会通过类com.sun.tools.javac.comp.Enter通过Visitor模式传入,在方法中
com.sun.tools.javac.jvm.ClassReader#enterClass将类型信息带到ExtendWrapper类生成的ClassSymbol的类型变量中,并将ClassSymbol存储在符号表中。
在代码生成过程的下一步中
com.sun.tools.javac.main.JavaCompiler#generate在方法中
com.sun.tools.javac.jvm.ClassWriter#writeClassFile通过符号表中ClassSymbol的类型,通过方法
com.sun.tools.javac.code.Types#supertype获取superClass的类型信息,通过ClassWriter写入到ClassFile的Signature属性中。
至此,我们了解了JavaCompiler是如何将泛型信息写入ClassFile的。这部分建议单步查看源代码(包 com.sun.tools.javac)。能理解的更透彻。
JRE(Java运行时环境)源代码
现在我们分析一下JRE中的反射API是如何将Signature字符串转换为Type类型的。我们注意到前面代码中主要使用的反射方法是Class类下的getGenericSuperclass方法。我们从这里开始分析。这是 1.5 中引入的方法,返回带有泛型的基类。其代码如下:
public Type getGenericSuperclass() {ClassRepository info = getGenericInfo();if (info == null) {return getSuperclass();}// Historical irregularity:// Generic signature marks interfaces with superclass = Object// but this API returns null for interfacesif (isInterface()) {return null;}return info.getSuperclass();}我们可以注意到,核心是第2行生成的ClassRepository类型变量,它代表了类的泛型类型信息。详情如下
此类表示类的通用类型信息。该代码不依赖于特定的反射实现。它被设计为至少可以由核心反射和 JDI 不变地使用。
深入getGenericInfo,我们可以看看信息是如何生成的。
private ClassRepository getGenericInfo() {ClassRepository genericInfo = this.genericInfo;if (genericInfo == null) {String signature = getGenericSignature0();if (signature == null) {genericInfo = ClassRepository.NONE;} else {genericInfo = ClassRepository.make(signature, getFactory());}this.genericInfo = genericInfo;}return (genericInfo != ClassRepository.NONE) ? genericInfo : null;}可以看到,字符串签名来自于方法getGenericSignature0,经过ClassRepository的处理,最终生成info。那么,这个字符串从哪里来呢?如果我们追踪一下,我们可以发现这是一个native方法。来自 JVM 实现。
// Generic signature handlingprivate native String getGenericSignature0();既然是JVM方法,我们可以通过查看源代码来验证它是否符合前面提到的JVMS标准。
OpenJDK源码验证
通过对JVMS的学习并结合JRE的Class#getGenericSignature0,我认为有必要探索一下JVM中的具体实现。根据上面使用的JavaCompiler源码,我们最终决定参考OpenJDK的实现。我们参考jdk8[12],内部使用比较广泛,
搜索函数名getGenericSignature0,可以发现文件./jdk/src/share/native/java/lang/Class.c中有一个JNI方法的配置数组
static JNINativeMethod methods[] = {{"getName0", "()" STR, (void *)&JVM_GetClassName},{"getSuperclass", "()" CLS, NULL},{"getInterfaces0", "()[" CLS, (void *)&JVM_GetClassInterfaces},{"isInterface", "()Z", (void *)&JVM_IsInterface},{"getSigners", "()[" OBJ, (void *)&JVM_GetClassSigners},{"setSigners", "([" OBJ ")V", (void *)&JVM_SetClassSigners},{"isArray", "()Z", (void *)&JVM_IsArrayClass},{"isPrimitive", "()Z", (void *)&JVM_IsPrimitiveClass},{"getComponentType", "()" CLS, (void *)&JVM_GetComponentType},{"getModifiers", "()I", (void *)&JVM_GetClassModifiers},{"getDeclaredFields0","(Z)[" FLD, (void *)&JVM_GetClassDeclaredFields},{"getDeclaredMethods0","(Z)[" MHD, (void *)&JVM_GetClassDeclaredMethods},{"getDeclaredConstructors0","(Z)[" CTR, (void *)&JVM_GetClassDeclaredConstructors},{"getProtectionDomain0", "()" PD, (void *)&JVM_GetProtectionDomain},{"getDeclaredClasses0", "()[" CLS, (void *)&JVM_GetDeclaredClasses},{"getDeclaringClass0", "()" CLS, (void *)&JVM_GetDeclaringClass},{"getGenericSignature0", "()" STR, (void *)&JVM_GetClassSignature},{"getRawAnnotations", "()" BA, (void *)&JVM_GetClassAnnotations},{"getConstantPool", "()" CPL, (void *)&JVM_GetClassConstantPool},{"desiredAssertionStatus0","("CLS")Z",(void *)&JVM_DesiredAssertionStatus},{"getEnclosingMethod0", "()[" OBJ, (void *)&JVM_GetEnclosingMethodInfo},{"getRawTypeAnnotations", "()" BA, (void *)&JVM_GetClassTypeAnnotations},};其中,getGenericSignature0对应的是JNINativeMethod结构体的一个对象。 {"getGenericSignature0", "()" STR, (void *)&JVM_GetClassSignature}
JNINativeMethod 定义如下
typedef struct {char *name;char *signature;void *fnPtr;} JNINativeMethod;可以看到getGenericSignature0对应的jvm实现是JVM_GetClassSignature,一个函数指针。
该函数的实现位于./hotspot/src/share/vm/prims/jvm.cpp中,包装在JVM_ENTRY宏中。
JVM_ENTRY(jstring, JVM_GetClassSignature(JNIEnv *env, jclass cls))assert (cls != NULL, "illegal class");JVMWrapper("JVM_GetClassSignature");JvmtiVMObjectAllocEventCollector oam;ResourceMark rm(THREAD);// Return null for arrays and primativesif (!java_lang_Class::is_primitive(JNIHandles::resolve(cls))) {Klass* k = java_lang_Class::as_Klass(JNIHandles::resolve(cls));if (k->oop_is_instance()) {Symbol* sym = InstanceKlass::cast(k)->generic_signature();if (sym == NULL) return NULL;Handle str = java_lang_String::create_from_symbol(sym, CHECK_NULL);return (jstring) JNIHandles::make_local(env, str());}}return NULL;JVM_END可以看到,最终的getGenericSignature0是通过InstanceKlass::cast(k)->generic_signature方法获得的。该方法使用序号_generic_signature_index从ClassFile的Symbol数组中获取相关数据。 javac编译过程的源代码符合JVMS标准。
// for adding methods, ConstMethod::UNSET_IDNUM means no more ids availableinline u2 next_method_idnum();void set_initial_method_idnum(u2 value) { _idnum_allocated_count = value; }// generics supportSymbol* generic_signature() const {return (_generic_signature_index == 0) ?(Symbol*)NULL : _constants->symbol_at(_generic_signature_index);}u2 generic_signature_index() const {return _generic_signature_index;}void set_generic_signature_index(u2 sig_index) {_generic_signature_index = sig_index;}对应jvm中Class文件格式中的Signature[11]
综上所述
我们从Java泛型开始,研究反射对泛型的扩展以及类型擦除的影响。然后通过生成匿名类实例的小技巧,我们获得了泛型运行时类型的技能。
然后,基于JVM标准和javac编译流程,我们从JRE源码入手,研究了JVM对泛型获取的实现,了解了底层原理。这个问题解决得比较好。
参考链接:
【1】:
【2】:
【3】:#jls-8.1.2
【4】:#jls-8.4.4
【5】:#jls-8.8.4
【6】:#jls-9.1.2
【7】:
【8】:
【9】:#jls-4.6
【10】:#jls-4.5
【11】:#jvms-4.7.9.1
【12】:
【13】:#heap_pollution
【14】
【15】
阿里云开发者社区,百万开发者的选择
阿里云开发者社区拥有数百万优质技术内容、数千门免费系统课程、丰富的体验场景、活跃的社区活动、行业专家的分享与交流。欢迎点击【阅读原文】加入我们。