Java 反射

在Java的开发环境中,运行java文件需要使用:java xx.java 命令,运行java命令后,便会启动JVM,将字节码文件加载到JVM中,然后开始运行;当运行java命令时,该命令将会启动一个JVM进程,在这个JVM进程中,会保存有该JVM创建的所有线程、变量、对象,这些线程、变量、对象会共享该JVM的内存区域。

当出现以下情况时,JVM进程会退出:

程序正常执行结束,JVM正常退出;使用System.exit(0)方法,JVM正常退出;程序运行出现异常,并且没有对异常进行处理(捕获、或者抛出)时;操作系统强制结束JVM进程,比如:关机、通过任务管理器强制结束等;JVM进程一旦退出,该进程中内存区域保存的数据(线程、变量、对象等数据)将会丢失,因为其都是保存在内存中的,并没有写入到硬盘。

01类加载机制

在Java 类的整个“漫长的”生命周期中,类的加载仅仅只是个开始,因为类在加载后还要经历一系列的处理,才能被JVM接受,并到处运行。

根据JVM规范,Java 程序的整个生命周期会经历5个阶段:加载 -> 链接(验证、准备、解析) -> 初始化 -> 使用 -> 卸载;因为在链接阶段会有验证、准备、解析三个步骤,所以也可以说Java 程序的生命周期会经历7个阶段(使用验证、准备、解析三个步骤来替代链接阶段)。

java字节码找调用链_java字节码找调用链

Java 程序的生命周期

当JVM要使用某个类某,而该类还未被加载进JVM内存中时,JVM会通过加载,链接,初始化三个步骤来对该类进行初始化操作,以便于后期运行。

1.类的加载

类的加载是指将类的class文件(字节码文件)载入JVM内存中,并为之创建一个java.lang.Class对象,也就是字节码对象。

类的加载过程由类加载器(ClassLoader)完成,类加载器由JVM提供,我们称之为系统类加载器,同时,我们也可以继承ClassLoader类来提供自定义类加载器。详情可查看:「JAVA」万字长篇详述字节码对象与反射机制完成动态编程

类的加载过程中也会对字节码文件进行验证,不过这里的验证并不会深入,而是把重点放在字节码文件的格式上;类加载器不仅可以加载本地字节码文件,加载jar包中的字节码,更是可以通过网络加载字节码。

2.类的链接

当类的字节码文件被加载进JVM内存之后,JVM便会创建一个对应的Class对象(也可以叫字节码对象),把字节码指令中对常量池中的索引引用转换为直接引用,接着把类的字节码指令合并到JRE中。链接包含三个步骤:

验证:检测被加载的类是否有正确的内部结构。准备:负责为类的static变量分配内存,并根据变量的数据类型设置默认值。解析:把字节码指令中对常量池中的索引引用转换为直接引用。

3.类的初始化

在完成类的链接之后,JVM便会对类进行初始化,这里的初始化主要是对static变量进行初始化,并非是类的实例化,因为类的实例化属于“使用”阶段。

类的初始化一个类包含以下几个步骤:

如果该类还未被加载和链接,则程序先加载并链接该类。如果该类的直接父类还未被初始化,则先初始化其父类,若是父类的父类还未初始化,就会先初始化父类的父类,依次类推,直至没有父类为止。如果类中有初始化语句(静态代码块),则JVM会依次执行这些初始化语句。

02运行时信息

运行时信息,即Runtime Type Information,是指程序在运行时的信息。在Java 中有两种方式可以得到运行时信息:

一是通过RTTI,即Run-Time Type Identification,这种方式假设我们在程序编写时就已经知道了所有对象的类型,主要是通过字节码对象Class来获取;二是通过“反射”机制,反射提供一组api,通过调用api,便能获取运行时的类信息;Java 中通过java.lang.reflect类库来支持反射;

03Class 对象

java字节码找调用链_java字节码找调用链_02

Class 类

Class类:我们平时在开发中定义的类是用来描述业务逻辑的;比如Teacher.java,Student.java等,而Class类用来描述我们所定义的业务逻辑的类,也就是描述类的类。

Class类的实例:其实就是JVM中的字节码对象,一个字节码文件有一个字节码对象,一个Class实例表示在JVM中的某个类或者接口,当然,也包括枚举和注解,因为枚举是一种特殊的类,注解是一种特殊的接口。

每一个类都有都有一个Class对象,也就是说每个类都有一个字节码对象,有一个字节码文件。当第一次使用类的时候,该类的字节码文件会被加载到JVM中,创建一个字节码对象;此时,该字节码对象就是一个Class实例。

既然每一个类都有一个Class对象,那么这些Class对象之间是如何区分它所表示的是哪一个类的字节码的呢?为了解决这个问题,Java 为Class提供了泛型:Class。

java.lang.String类的字节码类型:Class;java.util.Date类的字节码类型:Class;java.util.ArrayList类的字节码类型:Class;

创建Class对象

创建字节码对象,有3种方式:(使用java.util.Date类做演示)

1.使用类字面量,即使用类的class属性;

Class clazz1 = java.util.Date.class;2.使用对象的getClass();方法;

java.util.Date date = new java.util.Date();Class> clazz2 = date.getClass();3.使用Class.forName(String className);方法;

Class> clazz3 = Class.forName("java.util.Date");在上述的代码案例中使用了通配符“?”,通配符“?”表示“任何类”,通配符的使用使得Class对象的类型更加宽泛。

因为同一个类在JVM中只存在一个字节码对象,因此在上述的代码中存在:

clazz1 == cclazz2 == clazz3;

基本数据类型的字节码对象

在上述讲了三种获取Class对象的方式,都是类或者对象获取Class对象;但是基本数据类型不能表示为对象,不能使用getClass的方式;基本数据类型没有类名,所以也不能使用Class.forName的方式,那么该如何表示基本类型的字节码对象呢?

Java 中,所有的数据类型都有class属性,包括基本数据类型;如何获取呢?使用字面量的方式,这种方式不仅可以应用于类,还可以应用于接口、数组、和基本数据类型。语法如下:

Class clazz = 数据类型.class;详情如下:

java字节码找调用链_Class对象_03

基本数据类型的Class对象

预加载

在JVM启动期间,会预先加载一部分核心类库的字节码文件到在JVM中,其中就包括了九大基本数据类型。并且在8大基本数据类型的包装类中,都有一个常量:TYPE,用于返回该包装类对应基本类的字节码对象;

java字节码找调用链_java字节码找调用链_04

class & TYPE

那么如下的代码案例便解释得通了,因为Integer和int是不同的数据类型,所以会有不同输出结果:

java字节码找调用链_Java反射_05

Integer和int是不同的数据类型

数组的字节码对象

数组其实是对象,所以数的Class实例便可以使用引用数据类型的方式获取:

方式1:数组类型.class;

java字节码找调用链_运行时信息_06

数组类型.class

方式2:数组对象.getClass();

java字节码找调用链_运行时信息_07

数组对象.getClass();

注意:所有的具有相同的维数和相同元素类型的数组共享同一个字节码对象,和元素没有关系.

java字节码找调用链_java字节码找调用链_08

共享同一个字节码对象

Class类和Object类

Object:描述所有的对象,其拥有所有对象的共同的方法。Class:描述所有的类型,其拥有所有类型的相同的方法;需要注意:Class类也是Object的子类。每个类都有其字节码对象,通过字节码对象能获取到类的很多信息,在Class类中提供了很多的api来获取类的运行时信息,以下是一些常用的:

java字节码找调用链_Class对象_09

常用的字节码对象api

但是,上述的通过字节码对象获取类的信息是建立在我们提前知道确切的数据类型的基础之上的,如果我们事先不知道数据类型,那么便不能通过获取类的字节码对象来获取类的运行时信息;在这样的情况下,又该如何获取对象的运行时信息,该如何动态的创建类的对象?

对于这个问题,java.lang.reflect类库中提供了一个叫做“反射”的组件,可以配合Class类来共同提供动态信息获取,下面,就来一起看看吧!

04反射

java字节码找调用链_Class对象_10

何为“反射”

众所知周,对象有编译类型和运行类型。现有如下的代码案例:

java字节码找调用链_Java反射_11

编译类型和运行类型

需求:通过obj对象,调用java.util.Date类中的toLocaleString方法:obj.toLocaleString(); ,此时编译报错,如何解决编译错误?

案例分析:编译时,会检查该编译类型中是否存在toLocaleString方法;如果存在,编译成功,否则编译失败;此时编译失败是因为在Object类中没有toLocaleString方法,因此要将obj转换为java.util.Date类型,才能正确调用toLocaleString方法。

解决方案:因为obj的真实类型是java.util.Date类,所以我们可以把obj对象强制转换为java.util.Date类型:

java字节码找调用链_java字节码找调用链_12

解决方案

上述解决方案是在知道obj真实类型的情况下解决的,那么如果不知道obj的真实类型,就不能进行强转,也不能使用Class对象;此时,问题又该如何解决?

遇到此等问题,就不得不祭出法宝——反射了,在java.lang.reflect类库中提供了一组api用于提供对反射的支持。

反射:获取类的元数据(元数据:描述类的类的数据)的过程,在运行期间,动态的获取类中的成员信息,包括构造器、方法、字段、内部类、父类、接口等;并且把类中的每一种成员,都分别描述成一个新的类:

Class:表示所有的类;Constructor:表示所有的构造器;Method:表示所有的方法;Field:表示所有的字段;

在Java 中,Class类和java.lang.reflect类库共同提供了对“反射”的支持,在java.lang.reflect类库中就包含了Constructor、Method、Field等类,用于描述类的元数据,在字节码对象中提供了获取这些元数据的api:

java字节码找调用链_java字节码找调用链_13

获取这些元数据的api

05反射之构造器

获取构造器

在Class类中提供了获取类的构造器的方法:

java字节码找调用链_Class对象_14

获取类的构造器的方法

在上述方法中:

Constructor类表示类中构造器的类型,Constructor的实例就是某个类中的某一个构造器;参数 parameterTypes表示:构造器参数的Class类型,方法可借由传入的参数类型来获取对应的构造器;在Constructor类提供了用于通过反射获取到的构造器的来创建实例的方法:newInstance();

java字节码找调用链_Java反射_15

newInstance()

由于上述方法时在Class类中,所有在调用这些方法前需要先获取类的字节码对象,再通过字节码对象来调用获取构造器的方法。

先有Student类,在Student类中有三个构造器,分别是:无参构造器,公共带参构造器,私有构造器;代码如下:

java字节码找调用链_运行时信息_16

Student类

无参构造器:

java字节码找调用链_java字节码找调用链_17

无参构造器

公共构造器:

java字节码找调用链_类加载机制_18

公共构造器

私有构造器:在使用私有构造器创建实例时,由于私有构造器不能被外界访问,所以在创建实例前先设置其可以访问,案例代码如下:

java字节码找调用链_运行时信息_19

私有构造器

如果一个类中的构造器是外界可以直接访问(非private 的),同时没有参数,那么可以直接使用Class类中的newInstance方法创建对象示例,其效果等同于使用new关键创建对象。

06反射之方法

获取方法

在Class类中提供了获取类的方法的api,由于方法也是在Class类中,所以在获取方法之前,也是需要先获取其所在类的字节码对象,再来获取方法,最后再来执行方法。

Class类中获取类的方法的api:

java字节码找调用链_Java反射_20

Class类中获取类的方法的api

上述这些api的返回值均为Method类,Method类是用于描述反射中的方法的类,在Method类中,提供了执行反射获取的方法的方法invoke():

java字节码找调用链_Java反射_21

执行反射获取的方法的方法invoke()

下面,通过一个案例来实践上述方法的使用:

同样,会有Student类,类中有public方法,static方法、private方法,分别用于演示通过反射获取类中public方法,static方法,private方法;

Student类:

java字节码找调用链_java字节码找调用链_22

获取所有方法

java字节码找调用链_运行时信息_23

获取所有方法

调用public方法

java字节码找调用链_类加载机制_24

调用public方法

调用static方法,使用反射调用静态方法时,由于静态方法不属于任何对象,它只属于类本身。所以在执行静态方法时,传入invoke方法的第一个参数就不能是任何对象,在这里需要将其设置为null。

java字节码找调用链_运行时信息_25

调用static方法

调用private方法,在调用私有方法之前,要设置该方法为可访问的,因为Method是AccessibleObject子类,所以在Method对象中可以通过调用setAccessible(true);来设置访问可以访问。

java字节码找调用链_运行时信息_26

调用private方法

可变参数,方法的可变参数在底层是其实是作为数组处理的,所以在执行可变参数的方法时,可直接传入数组参数,但传参数,须得区分引用数据类型和基本数据类型;

java字节码找调用链_Class对象_27

可变参数

在本文中,介绍了类的加载过程,详细描述的了JVM对字节码文件的处理过程,从中也看到了不少JVM底层的处理细节。

字节码对象的创建使得RTTI得以获取大量被底层屏蔽的信息,通过这些信息能让我们更加了解Java 的设计思想。

通过反射提供的强大功能,不仅可以访问到类的运行时信息,还可以访问到类中不允许被外界访问的private属性和方法,这无异于又打开了另一个新世界的大门,我们能通过反射编写动态代码,使得编程更加灵活。这也是Java 语言的一大特色,能够很好的与C、C++这样的语言区分开来。

到这里就结束了,能完整看完的小伙伴,已实属不易,给你们点赞!由于作者知识有限,若有错误之处,还请多多指出!

完结。