知其然,知其所以然
0. 前言
在上一篇《反射从入门到精通之深入了解Class类》,我们深入分析了一下 Class 类的原理。在本篇文章,我们分析一下 Constructor 使用方法的原理。
1. Constructor
通过反射调用构造函数有两种方法:
- 调用无参构造函数:Class.newInstance()
- 调用带参数的构造函数:
- 通过 Class 类获取 Constructor
- 调用 Constructor 中的 newInstance(Object … initarges) 方法
具体可以详见《反射从0到入门》,知道了这些我们深入了解下 Constructor 中的 newInstance(Object … initarges) 方法。
1.1 newInstance
想要了解原理,第一步就是要看懂 jdk 的注释,newInstance 的注释如下:
(打扰了,看不懂这个,全剧终。。。)
别走,我来给你们翻译(Google 翻译真香)
使用 Constructor 代表构造函数,根据参数创建并且初始化一个实例。各个参数将自动拆箱以匹配原始形式参数,并且原始参数和引用参数必须根据需要进行方法调用转换。
要获取无参构造函数,参数长度可以为 0 或者是 null
调用非静态内部类,参数该。。。(此处不翻译)
访问通过并且参数检查成功,将继续进行实例化。如果构造函数的声明类没有初始化,需要初始化
构造函数完成,返回新创建并且初始化好的实例
根据小李这段粗糙的翻译中,可以得到下面几个关键的内容:
- 根据参数创建初始化实例,参数有匹配的规则;
- 获取无参构造函数,参数长度可以为 0 或者 null;
- 有访问权限并且对参数进行检查,需要获取到构造函数的声明类;
知道了这些我们来解读一下 newInstance() 的源码,看下图:
源码可以拆分为三块:
- 校验权限:校验权限就不在此分析了,大家可以自行查看源码
- 获取构造函数的声明类
- 创建实体
获取构造函数的声明类
构造函数声明类 ConstructorAccessor 是一个接口,如下图所示:
查看下接口的实现类如下结构(虚线代表实现接口,蓝色线代表继承,那白线是什么鬼?)
从图中可知实现类都是继承了 ConstructorAccessorImpl 抽象类,并且实现了 newInstance() 方法。
那到底使用哪个实现类那呢?咱们继续往下看
如果 ConstructorAccessor 已经被创建了,获取并赋值。如果没有则通过 newConstructorAccessor 方法创建 ConstructorAccessor。newConstructorAccessor 方法如下:
newConstructorAccessor 分为三部分:
- 检查是否初始化
这是反射工厂(ReflectionFactory)检查初始化状态,如果没有初始化会进行下面用红线圈上的操作。
那大概猜一下这块是做什么呢?
首先,inflation 字面理解是通胀或者膨胀,那 noInflation 按字面理解也是不膨胀。
Threshold 字面理解是阈值,inflationThreshold 按字面理解是通胀阈值,就是一个通胀的界限值。
按照字面理解可知 noInflation 来判断是否通胀,inflationThreshold 是一个通胀的界限值。
问问度娘,验证下咱们的结果:
JNI(Java Native Interface),通过使用 Java 本地接口书写程序,可以确保代码在不同的平台上方便移植。
猜的差不多,JVM 有两种方法来访问有关反射的类的信息,可以使用 JNI 读取器或者 Java 字节码存取器。inflationThreshold 是使用 JNI 存取器的次数,值为 0 表示永不从 JNI 存取器读取。如果想强制使用 Java 字节码存取器,可以设置 noInflation 为 true。
inflationThreshold 默认值是 15,如果不对 inflationThreshold 进行修改,JVM 访问反射的类的信息会先从 JNI 存取器读取 15次之后才会使用 Java 字节码存取器
这就可以解释通为什么要有一个初始化检测的操作了。
从这部分可以学到一些小知识:
我们可以使用 -D= 来设置系统属性,通过 System.getProperty("属性名称") 来获取属性值。
- 获取当前类的 Class 实例
-
根据条件获取 ConstructorAccessor
这么多 if 条件判断,不要慌,我来帮你分析一波:
-
第 1 步,校验在第二步获取的 Class 实例是不是抽象类,如果是抽象类就抛出异常。
-
第 2 步,判断是否是 Class 实例,因为 Class 实例的构造函数是 private,所以这块也需要抛出异常。
-
第 3 步,判断这个 Class 实例是否继承 ConstructorAccessorImpl,如果是父子关系,就调用 BootstrapConstructorAccessorImpl 创建 ConstructorAccessor,这个方法是调用 native 方法,底层用 c++ 实现的本地接口。
-
第 4 步,如果 noInflation 为 true 并且 Class 实例不是匿名的,需要调用 MethodAccessorGenerator.generateConstructor() 创建 ConstructorAccessor,具体的细节就不分析了,原理还是通过读取二进制文件,将 Class 实例加载进来,然后根据一些条件获取到想要的 Constructor。
-
第 5 步,上面的条件都不满足,就调用 NativeConstructorAccessorImpl,看下这个方法的源码:
调用次数和 inflationThreshold 比较,如果大于inflationThreshold(默认是 15 次),调用的方法是不是和第四步是相同的。
如果小于等于 inflationThreshold ,就要调用 newInstance0 方法,newInstance0 是 native 方法,调用的就是本地接口。
其实第一步初始化的时候就是为了在这里做铺垫呢。
到这里还没有完事,还有一个 DelegatingConstructorAccessorImpl 方法。
那这一块使用了一手代理模式,把 NativeConstructorAccessorImpl 放入到 DelegatingConstructorAccessorImpl 的 delegate 中。newInstance 调用的是 delegate 的 newInstance 方法。
还记得我最开始问白线是做什么的,这回解惑了吧。
内容有点多,我画个图带你们梳理一下:
创建实例
上一步获取到了 ConstructorAccessor 的实现类,直接调用 newInstance 方法去创建实例。
2. 总结
我们回顾下前面的内容:
首先我们根据 jdk 提供的注释知道 newInstance 可以根据参数进行初始化并返回实例,想要获取实例必须要获取到构造函数的声明类 ConstructorAccessor ,随后我们就深入分析了 ConstructorAccessor 。
ConstructorAccessor 有一个抽象类 ConstructorAccessorImpl,其它的实现类需要继承 ConstructorAccessorImpl,分别是下面几个实现类:
-
InstantiationExceptionConstructorAccessorImpl:将异常信息存起来,调用 newInstance 会抛出 InstantiationException 异常。
-
BootstrapConstructorAccessorImpl:当需要创建的 Class 实例和 ConstructorAccessorImpl 是父子关系,就要返回 BootstrapConstructorAccessorImpl,调用的是底层的方法,通过 C++ 编写。
-
SerializationConstructorAccessorImpl:也是一个抽象类,当 JVM 从 Java字节码进行读取,会返回这个实现类。
-
NativeConstructorAccessorImpl:调用本地接口方法创建 ConstructorAccessor ,需要根据调用次数和inflationThreshold 做比较,inflationThreshold 的默认值是 15,可以通过-Dsun.reflect.inflationThreshold=来修改默认值。
当调用次数大于 15次的时候,JVM 从 Java字节码进行获取。反之,从本地接口进行获取。
-
DelegatingConstructorAccessorImpl:代理类,是 NativeConstructorAccessorImpl 的父类。
获取到了 ConstructorAccessor,通过调用 newInstance() 方法来创建实例。
3. 彩蛋
反射相关文章:
公众号:Java知识学堂,里面有我最近整理的反射相关内容,希望能对大家有所帮助。