反射知识点总结
1.java有多少个类加载器?分别的作用是什么?一个Class文件是怎么被加载到JVM里的,描述一下加载流程。
答:java有三个类加载器,分别为:根类加载器,扩展类加载器,系统类加载器。根类加载器负责java核心类的加载,扩展类加载器负责扩展jar包的加载;系统类加载器负责自定义类的加载。
类加载的全过程包括加载、验证、准备、解析和初始化5个阶段。其中,验证、准备、解析三个部分统称为连接。
加载阶段。虚拟机利用类加载器将Class文件加载到内存中,准确的讲,是加载到内存中的方法区,并为这个Class文件生成一个Class文件对象(类的字节码对象),作为方法区中这个Class文件的访问入口。(注意,Class文件存放在方法区,但是Class文件对象并不一定存放在堆中,还可能在方法区中)
验证阶段。这一阶段的目的是为了确保Class文件中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
准备阶段。这一阶段是正式在方法区内为类变量分配内存,并设置初始值。
这里有两个特别容易混淆的概念需要强调一下。首先,这个时候进行内存分配的类变量指的仅是静态变量,而不是成员变量(实例变量),成员变量将会在对象实例化时随着对象一起分配在堆内存中。其次,这里的初始值指的是数据类型的零值(假设有一个类的静态变量为static int value = 123,那变量value在准备阶段过后的初始值为0而不是123)。
解析阶段。这一阶段是将类文件中的符号引用替换为直接引用。
符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。在java中,一个java类将会被编译成一个Class文件。在编译时,java类并不知道所引用的类实际地址,因此只能使用符号引用来替代。比如People类引用了Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假设是这个符号,当然,实际不是)来表示Language类的地址。
直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。直接引用就是类的实际内存地址,如果有了直接引用,那引用的目标必定已经被加载入内存中了。
初始化阶段。执行类中定义的java代码,初始化类变量和其它资源(假设有一个类的静态变量为static int value = 123,准备阶段过后,value初始值为0,而初始化阶段,就是执行java代码,将123赋给value,value的初始值为123)。
2.类加载的时机
答:什么情况下开始类加载过程的第一个阶段:加载?Java虚拟机规范中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。但是对于初始化阶段,虚拟机规范则是严格规定了有5种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):
1)遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
3)当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
总结如下:
类加载的时机?(什么情况下会将类文件加载到内存中?)
答:创建类的实例
调用类中的静态变量和静态方法。
初始化类的子类
对类进行反射调用的时候
虚拟机启动时,会初始化包含main()函数的那个类
3.什么是反射?
答:(官方概念)在程序运行状态中,对于任意一个类,都能够知道它的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性。这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。
通俗点讲:有一个类,我想访问这个类中的成员变量、构造方法和成员方法,正常情况下,我们拿到的是这个类的.java文件,那么我直接通过new关键字创建一个类的实例对象,然后通过这个实例对象就可以访问这个类的成员变量、构造方法、成员方法了。但是现在我没有这个类的.java文件,而是只能拿到了这个类的.class文件,那么如何去访问到这个类中的成员变量、构造方法和成员方法呢?这就是反射机制解决的问题了。
自我总结:还是不太好理解,为什么拿不到它的.java文件,却只能拿到.calss文件呢?这里的拿不到是指的程序本身拿不到.java文件对象。什么意思?1年前,我写了一个程序,该程序操作了一个类,名为Iphone,该类中封装了Iphone的一些功能。1年前后,Iphone的功能升级了,但是我却不能去改源码,那么我怎么能够将Iphone升级后的代码在程序中运行呢?反射给我们提供了解决的办法。我新建一个类,名为IphoneX,该类中封装的iphone的新功能,可是我不能改源码,也就是说程序本身不知道有IphoneX这个类,所以,自然无法通过new关键字来创建该类的实例对象,进而调用其中的方法。反射是怎么解决这个问题的呢?在一开始写程序的时候,就用反射的方式写,留一个接口来获取新增的类的字节码文件。当之后新增IphoneX这个类是,我们将IphoneX这个类编译,生成IphoneX的字节码文件,然后将该字节码文件传给程序(通过给该类的全路径,程序中有Class.forName(“类路径”)来接收),程序收到这个字节码文件之后,能够获取到这个该类的字节码文件对象,通过字节码文件对象来访问到该类中成员变量、构造函数、成员方法等内容。这有一点需要注意,程序中的代码是写死的,唯一灵活的地方就是类路径那里,我们可以通过配置文件来灵活的改这个类路径,但是其它的位置没法改,比方说Iphone类中的方法有发短信、打电话、听音乐等,那么你的IphoneX类中的方法也必须是发短信、打电话、听音乐等,你的方法名不能瞎写,因为一开始写程序时,调用的方法就是发短信、打电话、听音乐等,你写其它的方法,程序不认识。如何来保证这一点呢?通过接口来保证!
这样一来再看,我们刚开始写程序的时候,并不知道以后会有什么类,所以没有办法通过new关键字类创建该类的对象,进而访问对象的内容,有人说,我们把new关键字后面的类名也同过配置文件写成活的,不也一样吗?可惜,java没有提供这样的功能,new后面只能跟具体的类名,是死的!而字节码文件为什么是活的呢?因为java的反射提供了这样的功能。Class c = Class.forName(“cn.com.phone.IphoneX”);forName方法接收的参数是一个字符串,这个字符串代表的是类的全名,有人说,是类的全名,并没有体现出它的字节码文件啊?这是forName()方法内部封装的,我们没必要细究它的源码实现,但是它实现的思想,就是通过传进来的类全名,找到的是该类的字节码文件,通过字节码文件找到该类的字节码文件对象,并返回,也就是说,引用变量c指向的就是IphoneX的字节码文件对象。拿到了这个字节码文件对象就好说了。我们看到字节码文件对象的类型是Class,这是java专门提供的一个类,这个类里面封装了很多的系统方法,通过这些方法,我们就可以访问该类的成员变量、构造函数、成员方法了。这里注意,仅仅是访问奥。如果想修改类的成员变量,调用类的成员方法,必须要创建这个类的实例对象,而创建实例对象,必须要调用构造函数,就算是反射也不例外。通过调用Class类中的方法,可以解剖类的字节码文件对象,我们可以拿到字节码文件对象中的构造函数,然后调用该构造函数来创建类的实例对象,进而才能修改类的成员变量,调用类的成员方法等。
可以看到,在反射机制中,拿到Class类的字节码文件对象是非常重要的,那么如何才能获取Class类的字节码文件对象呢?总共有3种方式:
1) 实例对象.getClass()
2) 类名.Class
3) Class.forName(“类的路径”)
举例说明:
//方式1 Person p = new Person(); Class c1 = p.getClass();
//方式2 Class c2 = Person.class;
//方式3 Class c3 = Class.forName("cn.itcast_01.Person"); |
推荐使用3),因为第三种是一个字符串,而不是一个具体的类名,这样我们就可以把这样的字符串配置到配置文件中。1)和2)两种方式只适合在学习的时候使用,但是在实际开发中,一定是用第三种方式,如果使用1)和2)两种方式,反射就没有意思了,你还是要改源码。
自我总结:
要想创建一个对象,必须要调用类的构造函数,new关键字是直接调用类的构造函数来创建对象,反射机制是先获取到类的文件对象(类的字节码对象),然后对类的文件对象进行解剖,获取到构造函数,进而创建出类的对象。
值得注意的是,如果你只想查看类中的属性和方法,那么当你获取到类的文件对象之后,对类的文件对象进行解剖,就能够查看类的属性和方法,这时不需要创建对象。但是如果你想修改类中的属性或者调用类中的方法,那么当你获取的类的文件对象之后,必须通过类的文件对象创建出类对象,然后才能对类的属性和方法进行修改。
反射中,当你拿到了类的字节码文件对象,因为类的字节码文件对象的类型就是Class,所以,剩下的是就是Class类的使用了。
Class类中有如下方法:
getConstructors():取得本类中的全部构造方法
getInterfaces():取得本类所实现的全部接口
getSuperclass():取得本类的父类
getMethods():取得本类的全部方法
getFields():取得本类的全部属性
以上的方法都是查看类的结构。
Class类中有一个newInstance()方法,一般怎么使用这个方法呢?
首先通过Class.forName()方法获取Class的字节码对象,然后调用Class类中的newInstance()方法创建该类的实例化对象。
注意:使用newInstance()方法实例化对象的前提是该类必须存在一个无参构造方法,如果不存在,则肯定是不能实例化的。
如何通过反射创建对象?
答:Class对象中包括构造器(Constructor)、属性(Field)、方法(Method)。下面要讲的是通过反射来构造对应类的实例。
通过反射来生成对象有两种方式:
1、通过Class对象的newInstance()方法来创建Class对象对应类的实例。这个方法是使用Class对象对应类的默认构造器创建对象,这就要求Class对象对应类必须要有默认构造器。
2、使用Class对象获取指定的Constructor对象,调用Constructor对象的newInstance()方法来创建Class对象对应类的实例。这个方法可以使用Class对象对应类的任意指定的构造器来创建实例。
第一种方法比较常见,在spring这些框架中,会根据配置文件自动创建类的实例并注入到依赖此类的类中。这时候用的最多的就是默认构造器。像是在spring的配置文件中,我们提供的是某个类的全类名,这种情况要创建类的实例,就必须使用反射了。
如何通过反射获取和设置对象私有字段的值?
答:可以通过类对象的getDeclaredField()方法获取字段(Field)对象,然后再通过字段对象的setAccessible(true)将其设置为可以访问,接下来就可以通过get/set方法来获取/设置字段的值了。
如何通过反射调用对象的方法?
答:
大致需要以下四个步骤:
1. 获取当前类的Class对象。(通过forName()动态加载类)
2. 实例化这个Class对象。 (通过newInstance()方法)
3. 通过Class类的getMethod()方法取得Method对象,并设置调用方法时需要的参数类型。
4. 使用Method对象调用invoke()方法,并向该方法传递参数,其参数通常是一个类的实例化对象。