Java中如何获得A<T>泛型中T的运行时类型及原理探究
阿里妹导读
本文从Java的泛型开始,研究反射针对泛型的扩展,类型擦除的影响。然后通过生成匿名类实例的小技巧,获得了泛型的运行时类型的技巧。(后台回复【Java单元测试实战】可获取电子书)
简介
引言
public static <T> Class<T> typeOf(T obj) {
return (Class<T>)obj.getClass();
}
泛型(Generics)
add generics in java[14],并最终在1.5进入JDK。
/**
* 定义一个泛型类,其中
*
* Type Parameter是T extends Number
* Type Variable是T
* Type Argument是Foo<Integer>里的Integer
*/
class Foo<T extends Number> {}
反射(Reflection)
在实现上,反射引入了Type接口,以及派生接口和类,实现了泛型JLS的标准。它们的UML类型如下。
其中我们需要打最多交道的,就是ParameterizedType[10]
ParameterizedType可能是一个比较陌生的概念。但是经常用反射(Core Reflection )API的开发者可能比较熟悉,Type类型的一个派生类就是ParameterizedType。这个概念的非形式化(大白话)解释,可以简单的类比为泛型类型Foo<T>的一个实现。比如Foo<String>,Foo<Integer>就分别是Foo<T>的ParameterizedType
类型擦除(Type Erasure)
类型擦除的缺陷
Java泛型是Compile-time(编译期的)的,也就是说在运行时,所有泛型信息都被抹除了。所以JVM无法感知类型的存在。
所以我们无法通过反射API,在运行期获得Type Variable所代表的类型。
但是这个特性导致我们在写工具类时会遇到一些困难。比如无法单独通过T来创建实例。如果T是一个非泛型类还好,我们可以通过直接传入类型信息进行一些操作
public static final <T> void foo(List<T> list, Class<T> tClass)
但当T是一个ParameterizedType时,上述接口里的tClass类型信息,也只能获得ParameterizedType里非泛型的类型信息。比如T位List时,Class就是List.class。在一些场景,比如反序列化时,会遇到一些麻烦。
获取泛型的运行时类型的技巧
引入TypeReference
class Wrapper<T> {
}
它非常简单,基本就类似一个包装类。然后再做一个简单的方法定义
public static <T> Type getGenericRuntimeType(Wrapper<T> wrapper)
最后通过一个小技巧,就是创建匿名派生类的实例,配合反射API,先获取superClass的泛型信息,如果是ParameterizedType,就尝试获取真实的Type Argument信息,就可以获取T的运行时类型了。
public static <T> Type getGenericRuntimeType(Wrapper<T> 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 Wrapper<List<String>>());
Type type2 = getGenericRuntimeType(new Wrapper<List<String>>() {});
null
java.util.List<java.lang.String>
Classfiles need to carry generic type information in a backwards compatible way. This is accomplished by introducing a new “Signature” attribute for classes, methods and fields.
原理分析
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];
}
其中的attributes数组里,就是JSR14里提到的,泛型信息的保存所在。JVMS指出[11]
A Java compiler must emit a signature for any class, interface, constructor, method, or field whose declaration uses type variables or parameterized types
做一个小实验
public class ExtendedWrapper extends Wrapper<List<String>> {
}
javap后,可以看到42行类的Signature已有相应的类型信息了(Lcom/aliyun/cwz/model/Wrapper<Ljava/util/List<Ljava/lang/String;>;>;)。基本验证了JVMS标准
Classfile /Users/alibaba/myprojects/GenericsAndReflection/target/test-classes/com/aliyun/cwz/impl/ExtendedWrapper.class
Last modified 2023-4-17; size 413 bytes
MD5 checksum 96ca23aed30b94c2a445bbd76189e250
Compiled from "ExtendedWrapper.java"
public class com.aliyun.cwz.impl.ExtendedWrapper extends com.aliyun.cwz.model.Wrapper<java.util.List<java.lang.String>>
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#15 // com/aliyun/cwz/model/Wrapper."<init>":()V
#2 = Class #16 // com/aliyun/cwz/impl/ExtendedWrapper
#3 = Class #17 // com/aliyun/cwz/model/Wrapper
#4 = Utf8 <init>
#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<Ljava/util/List<Ljava/lang/String;>;>;
#13 = Utf8 SourceFile
#14 = Utf8 ExtendedWrapper.java
#15 = NameAndType #4:#5 // "<init>":()V
#16 = Utf8 com/aliyun/cwz/impl/ExtendedWrapper
#17 = Utf8 com/aliyun/cwz/model/Wrapper
{
public com.aliyun.cwz.impl.ExtendedWrapper();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method com/aliyun/cwz/model/Wrapper."<init>":()V
4: return
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/aliyun/cwz/impl/ExtendedWrapper;
}
Signature: #12 // Lcom/aliyun/cwz/model/Wrapper<Ljava/util/List<Ljava/lang/String;>;>;
SourceFile: "ExtendedWrapper.java"
那么Java编译器是如何操作的呢?
JavaCompiler探秘
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>)
com.sun.tools.javac.jvm.ClassReader#enterClass
com.sun.tools.javac.main.JavaCompiler#generate
com.sun.tools.javac.jvm.ClassWriter#writeClassFile
com.sun.tools.javac.code.Types#supertype
JRE(Java Runtime Environment)源码
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 interfaces
if (isInterface()) {
return null;
}
return info.getSuperclass();
}
我们可以注意到核心是第2行产生的ClassRepository类型的变量,它代表类的泛型类型信息。具体如下
This class represents the generic type information for a class. The code is not dependent on a particular reflective implementation. It is designed to be used unchanged by at least core reflection and JDI.
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字符串signature,通过ClassRepository的处理,最终产生了info。那么,这个字符串从哪里来呢?我们跟踪一下就可以发现,这是一个native方法。来自JVM实现。
// Generic signature handling
private native String getGenericSignature0();
既然是JVM的方法,那么我们可以翻一下源码来验证下是否符合之前提到的JVMS标准。
OpenJDK源码验证
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这个struct的一个对象。{"getGenericSignature0", "()" STR, (void *)&JVM_GetClassSignature}
JNINativeMethod定义如下
typedef struct {
char *name;
char *signature;
void *fnPtr;
} JNINativeMethod;
这个函数的实现在./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 primatives
if (!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 available
inline u2 next_method_idnum();
void set_initial_method_idnum(u2 value) { _idnum_allocated_count = value; }
// generics support
Symbol* 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;
}
结论
我们从Java的泛型开始,研究反射针对泛型的扩展,类型擦除的影响。然后通过生成匿名类实例的小技巧,获得了泛型的运行时类型的技巧。
然后根据JVM标准,javac的编译过程,通过JRE源码入手,研究了JVM对泛型获取的实现,了解了底层原理。比较好的解决了这个问题。
参考链接:
【2】: https://homepages.inf.ed.ac.uk/wadler/gj/Documents/gj-oopsla.pdf
【3】: https://docs.oracle.com/javase/specs/jls/se8/html/jls-8.html#jls-8.1.2
【4】: https://docs.oracle.com/javase/specs/jls/se8/html/jls-8.html#jls-8.4.4
【5】: https://docs.oracle.com/javase/specs/jls/se8/html/jls-8.html#jls-8.8.4
【6】: https://docs.oracle.com/javase/specs/jls/se8/html/jls-9.html#jls-9.1.2
【7】: https://docs.oracle.com/javase/1.5.0/docs/guide/reflection/enhancements.html
【8】: https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/class-use/Type.html
【9】: https://docs.oracle.com/javase/specs/jls/se8/html/jls-4.html#jls-4.6
【10】: https://docs.oracle.com/javase/specs/jls/se8/html/jls-4.html#jls-4.5
【11】: https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7.9.1
【12】: https://github.com/openjdk/jdk8u
【13】: https://docs.oracle.com/javase/tutorial/java/generics/nonReifiableVarargsType.html#heap_pollution
【14】https://jcp.org/aboutJava/communityprocess/review/jsr014/index.html
【15】https://openjdk.org/groups/compiler/doc/hhgtjavac/index.html
阿里云开发者社区,千万开发者的选择
阿里云开发者社区,百万精品技术内容、千节免费系统课程、丰富的体验场景、活跃的社群活动、行业专家分享交流,欢迎点击【阅读原文】加入我们。
微信扫码关注该文公众号作者