反射(Reflect)

定义

是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。

主要的类

类名 用途
Class类 代表类的实体,在运行的Java应用程序中表示类和接口
Field类 代表类的成员变量(成员变量也称为类的属性)
Method类 代表类的方法
Constructor类 代表类的构造方法

个人理解

反射作为Java的一种特殊的调用方法,单独使用一般只是为了调用对象的私有方法等,作用不是很大,也不建议这样使用。
但是反射结合泛型,注解等就可以爆发出强大的功能,比如 Gson的 泛型+反射 实现反序列化,Retrofit 的 动态代理+反射+注解,实现网络请求接口化。
所以在上面反射的主要类中,获取泛型、注解的 API 占了一半左右,如果需要用好反射,必须先精通泛型、注解。

如何学习

  1. 了解反射java.lang.reflect 包下的接口结构树,从接口入手学习。
  2. 因为反射涉及的API很多,所以具体使用还是得看的API,多敲敲代码看看各个方法实现了什么功能。
  3. 再看看各种框架中是如何使用反射的,一般都是结合了泛型、注解。

反射API

以下是JDK 16 的 Doc文档中的java.lang.reflect 包的接口结构树
2021-04-18_200110.png

Type 接口

Type is the common superinterface for all types in the Java programming language. These include raw types, parameterized types, array types, type variables and primitive types. Type是Java编程语言中所有类型的通用超接口。这些类型包括原始类型、参数化类型、数组类型、类型变量和基本类型。

实现子类:

  1. Class : 太熟悉了,类的类型,不多解释

继承Type的接口

  1. ParameterizedType :参数化类型 ,Collection<String> 之类
  2. TypeVariable :类型变量,T,K 之类
  3. GenericArrayType :数组类型,可以是上面2种类型的数组,Collection<String> [] , T[]
  4. WildcardType : 通配符类型 ? extends T 这类

Member 接口

实现该接口的4个子类 Class, Field, Method, Constructor 都是反射中最常用的
方法说明:

  1. Class<?> getDeclaringClass() : 返回的是获取到该Member的Class对象,与 getClass不同,(比如:返回的是具体类 com.xxd.reflect.basic.ReflectEntity,而不是 java.lang.reflect.Constructor )
  2. String getName() : 返回的是 getDeclaringClass().getName()
  3. int getModifiers() : 返回int类型,需要 & 来判断
  4. boolean isSynthetic() : 是否为了生成的方法,当前我知道的只有桥方法

AnnotatedElement 接口

  • JDK 5 添加进来的方法有4个
    1. boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) :检测当前上是否有某种Annotation
    2. <T extends Annotation> T getAnnotation(Class<T> annotationClass):获取某种注解
    3. Annotation[] getAnnotations() :获取当前上的所有注解
    4. Annotation[] getDeclaredAnnotations():获取当前上的所有注解,不包含继承类的注解
  • JDK 8 添加进来的方法有3个
    1. <T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass) :获取某种注解,不包含继承
    2. <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass) : 与@Repeatable相关,获取到所有可以关联到某种注解的注解
    3. <T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass) : 与@Repeatable相关,获取到所有可以关联到某种注解的注解,不包含继承

在 AnnotatedElement 接口中的方法,主要区分就是2点

  1. 是否 Declared,表示不能获取到继承的注解,在JDK 14 之前,只有类注解可以继承,其它都不行。
  2. 是否 ByType ,表示可以获取到关联的注解,什么是关联?涉及到一个元注解 @Repeatable

@Repeatable

可重复注释类型:用于指示它(元)注释的声明的注释类型是可重复的。@Repeatable的值表示可重复注释类型的包含注释类型。

  1. // 可以重复使用的注解
  2. @Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.CONSTRUCTOR})
  3. @Retention(RetentionPolicy.RUNTIME)
  4. @Repeatable(Ints.class)
  5. public @interface IntObtain {
  6. int value();
  7. }
  8. // 重复使用的注解会被装箱成该注解
  9. @Target({ElementType.TYPE,ElementType.FIELD})
  10. @Retention(RetentionPolicy.RUNTIME)
  11. public @interface Ints {
  12. IntObtain[] value();
  13. }

@interface IntObtain :可以重复使用的注解,以下简称注解A
@interface Ints : 接收注解A[] 为 value 返回值得注解,以下简称注解B
使用注意事项:

  1. 定义一个注解B,使用默认函数 value(不能是其它方法名),返回值是A的数组
  2. 注解A上必须加上 @Repeatable(B.class)标签,表示该注解重复使用会封装成B注解
  3. 注解A的target 范围必须大于注解B,因为注解A可以单独使用,而B不能
  4. 当使用1个注解A,不会自动装箱成B注解,而使用>=2个注解A则会自动装箱成注解B
  5. 接收的地方使用 ByType方法 ```java // 使用注解的地方 @IntObtain(52) @IntObtain(323) private int a;

// 测试2个不同的获取注解方法 IntObtain a1 = field.getAnnotation(IntObtain.class); IntObtain[] arr1 = field.getAnnotationsByType(IntObtain.class);

  1. 打印结果:
  2. > getAnnotation (IntObtain.class) : a1=null
  3. > getAnnotationsByType (IntObtain.class) : arr1.length= 2
  4. <a name="gixU0"></a>
  5. #### GenericDeclaration 接口
  6. 所有能定义泛型的地方都需要实现的接口,能定义泛型的地方只有3个,接口、类、方法<br />所以此接口的常用子类只有ClassConstructorMethod<br />`TypeVariable<?>[] getTypeParameters()` :获取所有的泛型定义处的 类型参数<br />`TypeVariable<D extends GenericDeclaration>`是一个有意思的类,它又通过泛型 D 获取定义此泛型的具体位置(就是上面那3个位置)<br />**tips: **所以能获取到TypeVariable的地方,都能获取到GenericDeclaration 的具体类型,Field上的T只能获取到接口类型,但是可以强转
  7. <a name="EcHYV"></a>
  8. #### AnnotatedType 接口
  9. 上面的接口已经解决了大部分问题,该有的泛型、注解都能获取到了<br />但是有一个地方的注解无法获取到,下面这种 `@Target(ElementType.TYPE_USE)`类型的注解可以定义在很多奇怪的地方,现有接口无法获取到
  10. ```java
  11. private @DoubleObtain(1.1d) List<@DoubleObtain(1.2d) ? extends @DoubleObtain(1.2d) T> @DoubleObtain(1.3d) [] list;

仔细看上面泛型的位置,好像与Type的4个子接口位置是对应的!!
因为@Target(ElementType.TYPE_USE)接口是JDK 1.8定义的,所以该 AnnotatedType接口也是 JDK 1.8 定义的,就是用来获取这些奇怪位置的注解。

tips: 只有定义成ElementType.TYPE_USE的注解才能被 AnnotatedType 找到,其它的注解放在方法前面也找不到,下面是一个例子说明

  1. @Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.CONSTRUCTOR,ElementType.TYPE_PARAMETER})
  2. @Retention(RetentionPolicy.RUNTIME)
  3. @Repeatable(Ints.class)
  4. public @interface IntObtain { // 这是一个没有 ElementType.TYPE_USE 的注解
  5. int value();
  6. }
  1. @Target(ElementType.TYPE_USE)
  2. @Retention(RetentionPolicy.RUNTIME)
  3. public @interface DoubleObtain { // 这是一个ElementType.TYPE_USE 的注解
  4. double value();
  5. }
  1. public void test(@IntObtain(110) @DoubleObtain(3.14) int a) { }

// 打印普通注解
Annotation[][] parameterAnnotations = method.getParameterAnnotations(); [@com.xxd.reflect.basic.domain.IntObtain(110)] 结论:找不到 ElementType.TYPE_USE 类型的注解,可以找到其它注解

// 打印 AnnotatedType 注解 AnnotatedType[] annotatedParameterTypes = method.getAnnotatedParameterTypes(); [@com.xxd.reflect.basic.domain.DoubleObtain(3.14)] 结论:只能找到 ElementType.TYPE_USE 类型的注解,找不到其它注解


反射的使用

Class<?>的获取

  1. object.getClass()
  2. Object.class
  3. Class.forName(…)

Class类型注意事项

  1. int.class != Integer.class, 基本类型8种都是 Primitive ,对应的是int.class == Integer.TYPE
  2. int.class != int[].class , 数组是包装类型,每多一层就不同
  3. 数组创建使用java.lang.reflect.Array类,里面有很多关于数组的创建,赋值方法
  4. Array 创建的数组,除了8种基本数据类型赋值为默认值,其它的数组最后一层都是 null,下面是一个说明的例子
    1. Object o = Array.newInstance(aClass1, 2, 3, 5);
    2021-04-22_004238.png
    与普通数组相似,数组类型的最后一层是真实类型了,需要初始化。而 Object[] 这类本身就是一个类型,是确定的类型,所以只有最后一层null

Gson到底做了啥

Gson是一个google出品的json格式字符串序列化、反序列化框架,它就是与反射相关。
那么Gson到底做了啥?

  1. 可以反序列化数组吗?int[] ? int[][]?
  2. 可以反序列化没有空参构造函数的类吗?
  3. 可以反序列化接口吗?抽象类呢?
  4. 在对于泛型的反序列化上,到底是怎么处理的?

通过分析上序问题,来了解反射的功能

问题1 :数组的反序列化

通过序列化一个数组,得到了如下json字符串

  1. {"arr":[3,5,1]}
  2. {"arr":[[312,64,13],[3,7,1]]}

2021-04-22_111839.png
反序列化是成功的,这还是自己对数组类型不够了解才产生了理解的困惑。
int[][][] ints = new int[2][][]; // 可以new
一个数组只有最外层需要定长 [2],而内部可以不定长 [ ],原因是数组类型本身就是一个类型,如 int[].class 本身就是一个类(不是 int.class),所以 int[].class 可以为 null。
具体来说 int[2][][] 表示的是 int[][].class 这种类型(以下记为 target.class), 就是由 2个 target.class组成了一个数组,所以2是必须定义的。至于target.class,本身怎么定义,是另外的事。
int[2][3][4] 这样是一种简便的创建方式,等于 target.class = int[3][4]
所以在反序列化数组的时候,不应该通过 Array.newInstance(aClass1, 2, 3, 5) 这种方式来创建,因为内部结构可能是不定长的。只能一层一层的创建,逐个解析。只要到达非数组类型的时候,值可以转为int,就不会报错。

问题2 :没有空参构造函数的反序列化

Gson是可以反序列化没有空参构造函数,并且不走其它构造函数。(如果有null参数构造函数,会走此构造函数)
反射构造一个类是必须要走构造函数的 (不走构造函数的class创建只有 clone, serializable,参考 java 对象创造的 5 中方式)
所以Gson反序列化一个类不是靠反射的,反射只是一个兜底机制,如果其它构件都不成功,最后走反射!

问题遗留:Gson到底是怎么创建一个对象的?创建一个具体类为什么可以没有 no-args的构造函数呢?
已经解决:参考附录
内容1:LinkedTreeMap 类,没有匹配到的类会被反射成此类

问题3 :接口、抽象类的反序列化

接口反序列化失败:Unable to invoke no-args constructor for interface com.xxd.reflect.gson.domain.IGun. Registering an InstanceCreator with Gson for this type may fix this problem.
找不到一个 no-args 的构造函数
抽象类反序列化也失败:
找不到一个 no-args 的构造函数
结论:接口、抽象类在处理在添加单独的 InstanceCreator 前无法反序列化。

问题4 :泛型的反序列化

泛型的擦除机制使得 Class 对象本身拿不到自己的实际泛型 signature 字段,所以直接用 .Class 处理某个json字符串,匹配不上的时候,会生成一个 LinkedTreeMap类,不断的嵌套自己,使得 Gson在处理反序列化的问题时数据不会丢失。
子类记录了父类泛型的实际 signature 字段,所以Gson 使用了一个Type type = new TypeToken<ClassT<ConcreteEntity>>(){}.getType()的方法,让使用者必须创建子类,所以能拿到对应 signature 字段,就匹配了具体的 Class。
使用API : getGenericSuperclass()可以获取到父类的Type,带有真实泛型,自己 new 子类,然后调用此API 。与new TypeToken<…>{ }.getType 等价。

手撸Retrofit

Retrofit2作为一个Android最常用的网络框架,使用的是 动态代理+注解+反射,同时适配了很多转换器 Gson , Rxjava 之类,真正用来网络请求的是 okHttp,当然作为代理,也可以换成其它网络框架,只要写一个适配器就可以了。
代码基本无难点,写了一个试试,一遍跑起来。

附录

Gson如何创建一个 没有空参数的对象

参考了Gson源码, com.google.gson.internal.UnsafeAllocator,使用的是此类来创建的。

Do sneaky things to allocate objects without invoking their constructors. 偷偷摸摸的分配对象内存,而不调用他们的构造函数

以上是UnsafeAllocator的说明,果然还是自己对java理解不深,以为java对象只有5种创建方式。
这里使用的是非安全创建方式,共有3种

  1. 使用Class.forName("sun.misc.Unsafe"),这是sun公司没有开源的API,需要手动分配内存,释放内存,无法GC,可以实现许多功能
  2. 使用ObjectStreamClass.class.getDeclaredMethod("getConstructorId", Class.class);,到内存中找是否已经构件过此类
  3. 使用ObjectInputStream.class .getDeclaredMethod("newInstance", Class.class, Class.class);,和上一个方法实现功能相同

这些方法都可以不调用构造函数创建对象。