Java 反射简介
Java 语言的标准库中包括了反射,名为 core reflection(核心反射),可以在运行时检索类的结构,包括在编译时不存在的类。同时也支持检索 Java 语言中的类型和注解。java.lang.Class
类上一些方法提供这些信息。这些模型类存在于 java.lang.reflect
包中。
类的结构
Java 语言中,类中的结构有字段(Field
)、方法(Method
)、构造器(Constructor
)、参数(Parameter
)、record component(RecordComponent
)。
对字段、方法、构造器,Class
中有命名如 get(Declared)Xxxs
方法批量获得此类结构,例如 getDeclaredFields
。
- 如果名称中包含
Declared
,获得的结构不包含从上级类继承的,但是包括所有本类中定义的此类结构,包括非public
结构。 - 否则返回的结构包括上级继承的结构(最优先本类定义结构,然后字段继承先接口(只有静态字段)再上级类,方法继承优先上级类再接口(接口静态方法无继承)详见 JVMS 5.4,构造器无继承),只包括
public
结构。(不包含从上级类继承的protected
结构)
同时还有 get(Declared)Xxx
接收参数,精准获得符合条件的结构。字段额外接收字段名称,方法接收方法名称和参数类型数组,构造器接收参数类型数组。能返回的结构必定存在于 get(Declared)Xxxs
返回中,如果无符合则报错。
Parameter
可以通过 Executable::getParameters
(方法和构造器的公共上级类中)获得,RecordComponent
可以通过 Class::getRecordComponents
获得。
这些方法返回数组,所以标准库每次返回时会复制一份数组。字段、方法、构造器还是 AccessibleObject
子类,这个类有 setAccessible
,是可变对象,所以这些结构在标准库返回时也会被复制一次。为了避免额外开销,使用这些方法时最好获取一次数组或者结构,然后缓存返回的数组或结构重复使用,避免获取和复制开销。
这些结构适合用来获得结构上的 Java 语言类型(例如泛型)和注解。字段、方法、构造器还提供方法获得及改变字段值和呼叫方法和构造器;这些用途方便一次性使用,但如果需要多次使用这些功能,使用 MethodHandles.Lookup
获得对应的 MethodHandle
或者 VarHandle
更合适,因为这些呼叫和改变方法每次使用时都会进行权限检查,影响性能。详见 java.lang.invoke
包相关介绍。
Java 语言中的类型
Java 语言定义(Java Language Specification)第四章定义了 Java 语言中的类型,核心反射也有模型类,为了和已有的 Class
兼容,和语言中的类型有比较复杂的对应关系。
各种结构中获得类型的接口 getXxxType(s)
返回 Class
类型,同时有 getGenericXxxType(s)
返回 Type
类型,可能是列表中的某一个模型类。
标准库返回的类型模型对象不可变,但是数组也是可变对象,有额外复制开销,所以如果返回的对象需要重复使用,推荐缓存数组或模型对象重复使用。
注解
注解可以携带自定义数据,可以存在于定义(结构)上(declaration annotation)或类型的用途中(type(-use) annotation)。携带类型的模型类都实现 AnnotatedElement
接口。以上提到的结构和类型都可以携带注解。核心反射能发现的注解都要有 @Retention(RetentionPolicy.RUNTIME)
元注解。
注解也有 get(Declared)Annotation(s)
的区分,类似结构;注解继承只存在于类与接口上,影响很小。Java 8 允许注解重复,有特殊的 get(Declared)AnnotationsByType
可以处理重复注解。
类型用途使用注解有一套模型类,和语言中的类型模型类相似但有些地方有细微差别,例如 AnnotatedArrayType
模型包含 int[]
,但在语言类型模型中不由 GenericArrayType
,而由 Class
模型此类型。
核心反射提供的注解皆通过代理(Proxy
)实现,可序列化。所以性能会有些劣势。因为返回的数组可变,有复制开销,推荐有需要缓存注解及其数组重复使用。
代理
为了方便 reflect 提供了一个代理工具,允许提供一个类加载器和一个接口列表,返回一个实现全部接口,类由指定类加载器加载的代理实例。代理的工厂方法还接收一个处理器,所有代理对象的呼叫(包括所有继承的抽象与非抽象接口方法和Object
的equals
、hashCode
、toString
方法)都会转到处理器。
这个工具过去常用于实现 AOP,现在推荐能编译时预处理可以考虑编译时预处理。它所有方法都要转到处理器,对 JIT 编译采样及其不友好,所以性能不堪入目;现在有新的类文件接口,条件允许可以生成自己的高性能动态类替代。