Java深渊处的秘密(第一遍总结:通俗解释反射原理)中我们已经从搜集的零碎资料大致知道了为何会使用反射,反射的原理,反射的简单使用。但是仍然没有建立一个系统的理解框架,在第二遍总结里,将从java反射的前世今生透彻的了解。就会有种豁然开朗的感觉。
学习经验介绍:知识需要反复琢磨,反射的知识不多也不难,但是我前前后后花了一周左右的时间才了解完,对同一个知识多达两三次的理解,都有不同的收获。如果第一遍不理解本文,建议收藏,过几天再看,会有意外的收获!
标题
1 什么是反射
建议先看 第九部分直接看收一下
语言的阐述总是枯燥折磨人的, Java深渊处的秘密(第一遍总结:通俗解释反射原理)已经通俗解释过了(建议跳转过去看看),这里我建议先从代码中直接感受一波,再理解言语解释更舒服~
2 体验反射的使用
读取配置文件调用Cat类的Sound方法
Class.forName(classfullpath)换成 Class.forName(“com.hspedu.Cat”)也可以
目的是获得:Cat类的完成类型名
准确来说 getclass()—返回运行类型
3 反射原理
- 为什么要把 Cat.class (字节码文件) 转化为 Class 类对象?
因为字节码文件中的信息系统是无法识别使用的,因此把这些信息处理之后,就变成了以数据结构的形式存储,比如
- 成员变量 以数组的形式存放在Fiied中------->Filed[] fileds
- 构造器 以数组的形式存放在Constructor中------->Constructor[] cons
- 成员方法 以数组的形式存放在Method中-------> Method[] ms
这些信息存放在堆中某个位置,为了方便管理,把这些与Cat类相关的信息定义为 Class类对象,当然每一个类有都有一个Class类对象,通称为Class类对象
- Cat.class (字节码文件)是通过什么方式变成可以被系统调用的Class类对象?
在java虚拟机中,提供了一个叫类加载器的东西,类加载器中(ClassLoader)封装了很多静态方法。有一个方法是loadclass(),这个方法可以把Cat的字节码文件映射处理一下,放到堆中(反射)
- 运行阶段是什么意思?创建对象是怎么回事? 既然堆中的Class类对象已经是可以调用使用了(以数据结构方式存储),我们就编写代码使用它。这个时候我们有两个方案
- Cat cat = new Cat() New完一个对象,我们点击运行按钮,系统就会从编写的Cat类读取信息,到字节码文件,再到堆中的Class类对象,再根据New这个指令在堆中开辟空间,把Cat类的从成员方法,成员变量等信息存进去(new
Cat开辟出的堆 因为更底层的设计 知道自己具体属于哪个 Class类对象- 通过编写可以利用反射方式的代码,系统就可以自动创建一个cat对象,但是我们是无法直接使用cat的,任何操作都是需要利用Class对象实现的(自动是指不利用new,不是啥代码都不写)
比如想使用cat的hi() 方法,利用Class对象创建一个Cat的对象cat,创建完了并不可以直接使用cat,然后继续利用Class对象调用cat的方法hi()---------实际代码中不出现Cat,hi,背后原理出现了Cat,hi
4 反射相关的主要类
在java.lang.reflection中
- java.lang.Class 代表一个类,类加载后在堆中的对象(字节码映射的那个)
- java.lang.reflect.Method 代表 某个类的方法
- java.lang.reflect.Field 代表 某个类的方成员变量
- java.lang.reflect.Constructor 代表 某个类的构造方法
5 反射调用优化
代码演示
setAccessible()是什么?
- Method
- Field
- Constructor 这三个的对象都有SetAccessible()方法,作用是启动和禁用访问安全检查的开关,参数値为true表示取消访问检查,提高反射效率
安全检查:检查
6 Class类
- Class也是类,继承Object类
(定位Class后快捷键ctrl+Alt+P-------调出父类)
- Class类对象不是New出来的,而是系统创建的
经过断点和追踪源码,发现Class.forName()可以追踪到类加载器的LoadClass方法(),参数name显示com.hspedu.Cat。
经过断点和追踪源码,发现new Cat可以追踪到类加载器的LoadClass方法(),参数name显示com.hspedu.Cat。
对比发现,无论是使用反射还是New,都可以成功跑到类加载器的部分,不就说明Class类对象是系统自己创建的吗
- 对于某个类的Class类对象,在内存中只存在一份
对于Cat类,无论创建几次Cat类的Class类对象,实际内存中只有一份,所以cls1==cls2
因为Cat类和Dog类不同,所以内存中cls2 != cls3
虽然New和反射都可以创建Class类对象,但是底层运作的原理还是有区别的,所以cls4 != cls1
探究原理:为什么内存中可以做到只保存一份?
假想一下,如果有十个线程调用同一个类,那么是不是在内存中保留十分?验证的结果说明,只保留一份。这说明这十个线程是同步的,一个线程干了,其他的线程就不是去干了。
具体来说是因为虚拟机会保证一个类的< clinit >()方法在多线程环境中被正确地加锁,同步。如果多个线程同时初始化一个类,那么只有一个线程去执行这个类的< clint >()方法,其他线程需要阻塞等待。直到活动线程执行< clint >() 方法完毕。
-
每个类的实例(cat)都会记得自己是由哪一个Class类对象产生的
-
通过Class类对象可以完整的得到一个类的完整的结构
-
类的字节码二进制数据(又称为类的元数据:方法代码,变量名,方法名,访问权限…)存放在方法区
为什么要存放在方法区?
为了提高效率,试想一下,如果有上百个类,就有上百了类对象,那么程序运行起来岂不是非常消耗时间,因此提出了一个优化方案:把类的具体数据单独存放起来,放在方法区(相当于武器库里的存放所有士兵的武器装备)。在堆里面存放类对象(士兵名单)。
上百个士兵中,我只要找到士兵A的武器:先看一遍写有上百个士兵的名单,这很快,找到了士兵A,然后取武器库找到存放士兵A武器的区域。这比直接从武器库找属于士兵A的武器效率高太多了。
7 Class类对象常用方法
8 获取Class对象的六种方式
之所有会有这么多的方式 是为了在不同的情况下方便使用
方法2: 参数传递的意思是:比如Cat类的有参构造函数 public Cat(String name)
目的:调用有参构造函数
需要这样传递参数:Constructor constructor2 = cls.getConstructor(String.class);
9 静态加载和动态加载
- 静态加载
如果代码中调用了一个不存在的类,静态加载是无法通过编译的。
- 动态加载
如果利用动态加载(反射),可以通过编译。
我们在控制台输入2,并不调用不存在的Fish类,照样可以运行
但如果控制台输入1,此时才会编译处理Fish类,返现不存在,才报错(使用的时候才处理)
10 类加载过程
当你写完代码后,点击运行,你得代码源文件就按照上图的流程一步一步执行,下面对部分名词进行解释,方便理解图表达的每个信息。
案例一:借助这段代码
public static void main(String[] args){
static int a=10;
static int b=20;
static String s = "zjh"
}
类加载第二个阶段 :连接阶段的准备--------对静态变量默认初始化:
- a = 0
- b = 0
- s = null
类加载的第三个阶段:初始化:
- int a=10;
- static int b=20;
- static String s = “zjh”
案例二:
11 通过类对象-获取类结构信息
12 Filed、Method、Constructor细节补充
13 反射爆破创建实例
为何使用爆破?
因为我们想要使用 非public 的属性,构造器,方法,但因为没有权限 就强行 给一个权限去使用
什么是爆破?
setAccessible(true):说明可以访问一切
怎么使用?
下面代码将从Filed,constructor,method三个方面介绍
实体类准备
构造器
Filed
Method