编译软件:IntelliJ IDEA 2019.2.4 x64
运行环境:win10 家庭中文版
jdk版本:1.8.0_361
一. 反射是什么?
我们之前写代码的步骤:
先在编译期间,先确定要创建的对象的类型,然后用new关键字去创建对象,并且也是在编译期间确定它要调用的方法,或者要操作的属总结: 在编译时什么都已知并且确定了。
在之后写代码可能会遇到这样的问题?
在编译期间,或者写代码的时候,还不能确定你要new的对象的类型是什么? 所以就无法直接使用new表达式创建对象.
或者说,在编译时还不能确定要调用对象的哪个方法,或者操作哪个属性,就无法写 对象.方法,对象.属性表达式。
例如: JDBC,Java代码操作数据库
现在需要写一段程序,来读取数据库中的数据。
但是还不清楚,你要操作的数据库的数据是什么,可能是一个学生表,可能是一个员工表,可能是一个订单表…
就是无法确定在Java中需要new什么对象。
问题的根本原因,需要在运行时才能确定,你要创建什么类型的对象,调用什么方法,或者为什么属性赋值。
原来: 已知的Java类 --> 获取类的信息
现在: 在运行时去动态的获取某个类 CLass对象–>通过CLass对来获取类的信息–> 创建对象
二. java.lang.Class类
要想解剖一个类,必须先要获取到该类的Class对象。而剖析一个类或用反射解决具体的问题就是使用相关AP!
(1) java.lang.Class
(2) java.lang.reflect.*。
所以,🔺Class对象是反射的根源。
2.1 哪些类型可以获取class对象?
Java所有类型都有CLass对象,包括基本数据类型、void、引用数据类型(数组、类、接口、枚举、注解等)。
2.2 如何获取一个类的class对象?
1.最简单但是有局限性,要求编译时这个类型就是已知,
已存在的类型类型名.class
代码演示如下:
public class TestClass { @Test public void test01(){ //基本数据类型 Class c1 = int.class; //void 空类型 Class c2 = void.class; //数组 Class c3=int[].class; //接口 Class c4=Comparable.class; //String Class c5="hss".getClass(); //自定义类型-->自己写的类 Class c6=TestClass.class; //注解 Class c7=Test.class; //类类型 Class c8=Math.class; System.out.println("基本数据类型int:"+c1); System.out.println("空类型void:"+c2); System.out.println("数组int[]:"+c3); System.out.println("接口Comparable:"+c4); System.out.println("String:"+c5); System.out.println("自定义类型TestClass:"+c6); System.out.println("注解Test:"+c7); System.out.println("类类型Math:"+c8); } }
2.只针对引用数据类型
对象名.getCLass()
代码演示如下:
@Test public void test02(){ //String Class c5="hss".getClass(); System.out.println(c5); }
3.通用的方法,一般是针对普通引用数据类型,
一般数组、基本数据类型这种内置的类型或动态编译生成的类型不会用它获取
cLass.forName(“类型的全名称”)
@Test public void test03() throws ClassNotFoundException { Class c1 = Class.forName("java.lang.String"); System.out.println(c1); }
4.一般很少用,一般用于自定义的类加载器类加载器对象.Loadclass("类型的全名称”)
ClassLoader类中有一个方法getSystemClassLoader获取当前系统的默认的类加载器对象
@Test public void test04() throws ClassNotFoundException { ClassLoader c1= ClassLoader.getSystemClassLoader();//得到系统默认的类加载器对象 Class c2=c1.loadClass("java.lang.Thread"); System.out.println(c2); }
💡小tips:
针对同一个类型来说,以上四种方式获取到的CLass是同一个。
也就是说每一种类型在内存中只有唯一的一个CLass对象。
三. 类加载
任何一个类的class对象并不是由程序员手动创建,而是类在加载时自动产生
类在内存中完整的生命周期: 加载–使用–>卸载
一般不会考虑到卸载这一步
3.1 类的加载过程
当程序主动使用某个类时,如果该类还未被加载到内存中,系统会通过加载、连接、初始化三个步骤来对该类进行初始化,如果没有意外,JMM将会连续完成这三个步骤,所以有时也把这三个步骤统称为类加载。
类的加载又分为三个阶段:
(1) 加载: load
就是指将类型的class字节码数据读入内存
(2) 连接: link
①验证:校验合法性等
②准备: 准备对应的内存(方法区),创建Cass对象,为类变量赋默认值,为静态常量赋初始值
③解析: 把字节码中的符号引用替换为对应的直接地址引用
什么叫“符号引用"?
比如:String str=“Hello world”;
String 就是 符合引用
符号引用 | 类和接口的全限定名 |
字段的名称和描述符 | |
方法的名称和描述符 |
举例:str是个字符串,就会在字符串常量池中找到’'Hello world”的地址值,将前面的符合引用(String)替换为
''Hello world”的地址值,这样程序运行时就不用线性寻址,加快程序运行速度。即使类加载时会慢一点,但在运
行时速度会很快。
(3)初始化: initialize (类初始化)即执行类初始化方法,大多数情况下,类的加载就完成了类的初始化,有些情况下,会延迟类的初始化。
四. 类初始化
4.1 哪些操作会导致类的初始化?
(1) 运行主方法所在的类,要先完成类初始化,再执行main方法
(2)第一次使用某个类型就是在new它的对象,此时这个类没有初始化的话,先完成类初始化再做实例初始化
(3) 调用某个类的静态成员(类变量和类方法) ,此时这个类没有初始化的话,先完成类初始化(4)子类初始化时,发
现它的父类还没有初始化的话,那么先初始化父类
(5) 通过反射操作某个类时,如果这个类没有初始化,也会导致该类先初始化
🔔类初始化执行的是0,该方法由 (1) 类变量的显式赋值代码(2) 静态代码块中的代码构成
4.2 哪些代码会让类初始化延迟进行?
(1)如果通过一个子类使用从父类继承的静态变量,静态方法时。延迟子类的初始化。
(2) 如果使用某个类声明数组,使用数组,不会导致这个类初始化。
(3) 如果使用某个类的静态的常量,也不会导致这个类初始化。
代码演示如下:
public class TestMethod { public static void main(String[] args) { System.out.println(son.a); //定义长度为4的son类数组 son[] sons=new son[4]; System.out.println(sons.length);//只是得到的是son类数组的长度,son类数组sons里没有装任何对象 System.out.println(son.MIN);//该类的常量早在类的加载中在执行第二步连接阶段准备部分就已被赋默认值,不需要进行初始化 } } class fa{ static int a=1; static { System.out.println("fa.静态代码块"); } } class son extends fa{ public static final int MIN=10; static{ System.out.println("son.静态代码块"); } }
五. 类加载器
5.1 类加载器的类型 ClassLoader类
(1) 引导类加载器
: 负责加载最最核心的类库,例如: JRE的核心类库中的rt.jar等.
如何查看JRE的核心类库中的rt.jar包?
请看如下:
(2) 扩展类加载器
:负责加载JRE目录下lib 文件中的ext扩展库
如何查看JRE目录下lib 文件中的ext扩展库?
找到自己jdk安装目录–>jre–>lib–>ext
如下所示:
(3) 应用程序类加载器
: 负责程序员自己编写的类、接口等
例如: TestClassLoader是我们自己写的
(4) 自定义类加载器
:
通常情况下我们是不需要自定义类加载器,
但是如果你的系统有如下两种情况可以自定义类加载器:
A: 字节码文件需要加密,自定义类加载器在加载类的过程中先解密,然后再创建CLass对象等。
B:自定义的类或类路径是在特殊的,特定的路径,和平时其他项目的类路径不同。
例如: tomcat服务器,它的classes是放在 WEB-INF/classes 文件夹
有的系统是把字节码文件放到网络中的某个数据库中,或者是某台文件服务器,和程序运行不
个服务器。
5.2 这些类加载器是如何一起工作?
我们这里主要讨论 (1) (2) (3)
它们之间的工作模式被称为**“双亲委托模式”**。
🤮设计目的是为了安全。
双亲:
生活中:父母双亲
这里是: parent属性
每一个类加载器都有一个parent属性,记录父加载器。
🤮类加载器工作流程如下:
A:当系统需要加载某个类时,同时时应用程序类加载器先接到任务,例如: 要加载java.lang.String类型。
B:应用程序类加载器接到任务后,会先在方法区搜索这个类的CLss对象,如果找到了,说明这个类已经被加
载过了那么就直接返回它的CLass对象,不会重复加载。如果没有找到这个类的CLass对象,会把这个任务先
提交给“父”加载器应用程序加载器的父加载器就是扩展类加载器。
C:扩展类加载器接到任务之后,会在方法区搜索这个类的CLass对象,如果找到了,说明这个类已经被加载过
了那么就直接返回它的CLass对象,不会重复加载。
如果没有找到这个类的CLass对象,会把这个任务先提交给“父”加载器
扩展类加载器的父加载器就是引导类加载器,也称为根加载器
D:引导类加载器接到任务之后,会在方法区搜索这个类的CLass对象,如果找到了,说明这个类已经被加载过
了那么就直接返回它的CLass对象,不会重复加载。
如果没有找到这个类的CLass对象,会把在自己负责的区域加载这个类,例如它负责rt.jar等。
如果找到了,就返回它的CLass对象,
如果没找到,就把任务往回传,传给扩展类加载器
E: 扩展类加载器接到父加载器回传的任务,就会在他负责的目录下加载这个类,例如ire/ib/ext如果找到了,
就返回它的CLass对象,如果没找到,就把任务往回传,传给应用程序类加载器F;应用程序类加载器接到父加
载器回传的任务,就会在他负责的目录下加载这个类,例如:项目路径下(idea中就是out目录)如果找到了,就
返回它的CLass对象,如果没找到,就报错CLassNotFoundException