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

阿里巴巴简介

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

本文从Java泛型开始,研究反射对泛型的扩展以及类型擦除的影响。然后通过生成匿名类实例的小技巧,我们获得了泛型运行时类型的技能。 (后台回复【Java单元测试实践】即可获取电子书)

介绍

如果你经常写工具类,很可能会遇到一个实际问题,就是需要获取泛型表达式A中T的运行时类型。获取它需要一些技巧。但很少有人彻底解释这项技术为何有效。在下一篇文章中,我们将从Java泛型(Generics)开始,结合JLS(Java Language Standard,Java语言规范)和JVMS(Java Virtual Machine Standard,Java虚拟机规范),并使用javac编译过程来生成源代码泛型处理的思路,结合对JRE反射API源码的探索,最终用虚拟机实现(OpenJDK8的Hotspot)进行了验证,从根本上回答了这个问题。

介绍

在编写工具类或者使用通用简化逻辑时,往往需要获取运行时的类型信息来进行下一步的逻辑判断,比如业务中经常使用的插件架构,或者反序列化时获取类型信息。细节。例如,应该有很多新手会尝试编写下面的代码,通过反射和泛型来获取某个类型的运行时类型。

public static  Class 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 final  void foo(List list, Class<T> tClass)

但当T为ParameterizedType时,上述接口中的tClass类型信息只能获取ParameterizedType中的非泛型类型信息。例如,当T在List中时,Class是List.class。在某些场景下,比如反序列化,你会遇到一些麻烦。

获取泛型运行时类型的提示

引入类型参考

那么,在Java中,有没有办法获得通用的运行时类型呢?答案是肯定的。但我们需要做出一些改变。例如,我们可以在很多序列化框架(如Jackson、Fastjson)中看到TypeReference或类似的设计机制。在“基本上”不改变函数签名的情况下获取 T 的运行时类型。

我们定义类

class Wrapper {}

它非常简单,基本上就像一个包装类。然后做一个简单的方法定义

public static  Type getGenericRuntimeType(Wrapper wrapper)

最后还有一个小技巧,就是创建一个匿名派生类的实例,配合反射API首先获取superClass的泛型信息。如果是ParameterizedType,则尝试获取真实的Type Argument信息,即可获取T的运行时类型。

public static  Type 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.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>  minor version: 0  major version: 52  flags: 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: ()V    flags: ACC_PUBLIC    Code:      stack=1, locals=1, args_size=1         0: aload_0         1: invokespecial #1                  // Method com/aliyun/cwz/model/Wrapper."":()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;>;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.Iterableextends 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 interfaces if (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 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; }

对应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】

阿里云开发者社区,百万开发者的选择

阿里云开发者社区拥有数百万优质技术内容、数千门免费系统课程、丰富的体验场景、活跃的社区活动、行业专家的分享与交流。欢迎点击【阅读原文】加入我们。

你可能想看:
« 上一篇
下一篇 »

相关推荐

java中什么是冒泡,冒泡算法

2026年09月06日

27阅读

java中冒号,java双冒号

2026年09月06日

26阅读

java中的->,java中的是什么意思

2026年09月06日

24阅读

java中的super用法,Java中super的说明

2026年09月06日

28阅读

java中反射的应用,java中反射的应用场景

2026年09月06日

25阅读

java中开关的定义,Java关键字有哪些

2026年09月06日

25阅读