持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情
反射,反射,程序员的快乐。最近在《大话设计模式》书中看到这句话,但是学习反射,我感觉我之前并不快乐!
对于这篇文章,是不准备讲武德了,我就觉得反射对初学者不太友好!(嘴巴上diss一哈就好了,反射还是要认真学的)。
学习反射的最主要目的是深入spring等框架底层,这是程序员进化成程序猿的必经之路。举个例子说下为什么反射难理解,假设你爹妈从小就跟你说是先有鸡后有蛋,上了大学老师告诉你是先有蛋后有鸡,这时候你就会不理解,因为在很长一段时间里,你的观点就是先有鸡后有蛋。你很难理解为什么先有蛋后有鸡,下面讲到小林自己对反射的理解时,大家可以结合这个案例理解。
首先是文章的内容介绍:
- 前驱知识铺垫1:JVM的启动与关闭
- 前驱知识铺垫2:类的加载机制
- 为什么要有反射
- 我所理解的反射
- 让我恍然大悟的Class类的实例
- 反射获取构造器及反射创建对象
- 反射获取类中方法及反射调用方法
- 反射获取成员变量及成员的读写
- 新发现:反射调用静态方法和数组参数
- 补充:Class类中的其他API
1.JVM的启动与关闭
启动
当我们执行java 类名这个命令时,我们可以清楚的看到在文件夹中会生成一个对应的.class文件,这个文件称之为字节码文件。 当使用javac命令来运行某个字节码文件时,该命令将会启动一个JVM进程.
关闭
JVM在四种条件下会关闭:
- 程序正常执行结束.
- 使用System.exit(0)方法;
- 出现异常时,没有捕获异常.
- 平台强制结束JVM进程.(任务管理器直接结束进程)
2.类的加载机制
初学的时候我是这么理解的:类的加载就是由.java文件变成.class文件,然后由JVM加载进内存创建字节码对象。(或许很多人都跟我差不多的认知吧)
首先先放一张图,这张图来源于网络,应该是挺详细的一张表,新手建议跳过这张图
解释一哈上面的图:
在学HelloWorld时,大家都知道一个.java文件通过编译.class文件,然后就可以创建对应类的对象。其实刚开始学,能理解到这,已经非常厉害了。接下来详细看看整个类的加载。
首先,当程序主动使用到某个类时,如果该类还未被加载进内存中,则系统会通过加载,连接,初始化三个步骤来对该类进行初始化操作。
类的加载:类加载是指将类的class文件(字节码文件) 载入内存中,并为之创建一个java.lang.Class对象,我们称之为字节码对象.类的加载过程由类加载器(ClassLoader) 完成,类加载器通常有JVM提供,我们称之为系统类加载器,我们也可以继承ClassLoader类来提供自定义类加载器.(这边自定义加载器我们暂时不深究等到后面学习框架自然会懂)。
类的连接:当类被加载进内存之后,系统为之生产一个对应的Class对象,接着把类的二进制数据合并到JRE中.这个过程有验证,准备,解析三部分。
- 1 验证:检测被加载的类是否有正确的内部结构.
- 2 准备:负责为类的static变量分配内存,并设置默认值.
- 3 解析:把类的二进制数据中的符号引用替换为直接引用.
符号引用与直接引用目前还不能用通俗的语言表达出来,后面会尽力完善。
类的初始化:在此阶段,JVM负责对类进行初始化,主要就是对static变量进行初始化.
- 1 如果该类还未被加载和连接,则程序先加载并连接该类.
- 2 如果该类的直接父类还未被初始化,则先初始化其父类.
- 3 如果类中有初始化语句(静态代码块),则系统依次执行这些初始化语句.
3.为什么有反射?
从类的本质考虑
初学Java面向对象时,肯定都听过万物皆对象这句话。例如生活中的猫咪,二哈,筷子,碗都能用一个类来描述。既然万物都有类能够来描述它,那么如果把这些类当成对象,我们又用什么类来表示它呢?
从类型强转考虑
先提两个概念,第五行代码中等号左边的Object是编译类型,等号右边的java.util.Date是运行类型。假设有这么一个需求,我要用第四行的obj对象调用java.util.Date类中的toLocaleString方法。
通过第二张图,我们可以看到类型没有强转的编译报错了,加了强转的没有编译错误。那么造成这个现象的原因是什么呢?这涉及到Java中的编译检查,在obj对象调用toLocaleString()方法时,会检查编译类型中是否存在该方法,发现没有自然就编译报错了。这个错误的解决办法我相信大家都会,就是强转类型。
那么问题来了,如果我们不知道obj的真实类型,那又该如何?我该强转成什么类型?这里有有人会想,这不是有眼就行,这都看到了是Date对象的,为啥就不知道真实类型。首先我们先看个伪代码:
// 底层有一个方法会返回一个Object对象,但是实际代码不是这样的。这边要表达的
// 的意思是它返回的是一个Object类型的,这个底层代码我们看不见吧。我们拿到就是
// 方法的返回值Object类型
public static Object getObject(){
return new java.util.Date();
}
4.我所理解的反射
通过上面的学习,我们反射的引入已经结束了。接下来谈谈我对反射的理解,这边的理解可能不到位,就是我学习完反射之后自己的理解,若有错误,望指正,不胜感激!
中国文化博大精深,很多时候不明白一个词语什么意思,我们给原来的词语加加字就会恍然大悟。由此我形成了自己的理解:反射的命名在我的理解就是反向映射,既然有反向映射,那么是不是就存在正向映射,万物都具有两面性。现在聊聊正向反射跟反向反射
所谓的正向映射我们暂且理解为就是通过类去创建对象。这个过程是类先加载进内存,然后通过产生的字节码文件去创建对象。这是一个由类到对象的过程。
正向映射是类到对象的过程,那么反向映射是不是就是通过对象去获取类呢?那么这个对象是什么对象呢,这个对象就是我们的Class类型的对象,通过Class类型的对象上的方法,就可以获取到类。
5.让我恍然大悟的Class类的实例
简单说两个概念
Class类:Class类是用来描述类或者接口的类型,描述类的类
Class类的实例: 在JVM中的一份份字节码,Class实例表示在JVM中的类或者接口,枚举是一种特殊的类,注解是一种特殊的接口.(这个不是瞎掰的,API就是这意思)
5.1Class实例是如何创建的
当我们需要使用到某个类时,这个类的字节码文件就会加载进JVM并创建对应的Class对象。当程序第一次使用某一个具体类的时候,就会把该类的字节码对象加载进JVM,并创建出一个Class对象.此时的Class对象就表示具体类的字节码.但是在内存中,字节码对象只存在一份。 不理解这句话没事,我们上代码。
从图中我们可以看到创建字节码对象的三种方式,并在后面用==将三个对象进行比较。运行结果足以说明一切,每个类的字节码对象在内存中就存在一份,它在需要用到它的时候加载进内存。
注意:为了区别Class类表示那个字节码,Java中提供了泛型Class
5.2基本数据类型的Class实例
这个也算是我的一个收获,我们用的类都要加载进内存生成字节码对象才可以使用。那么我们天天用的基本类型就可以为所欲为不用进内存吗?实际上,我们在启动一个程序时,就会产生一个JVM进程,在进程产生时会自动给我们加载好九个内置的Class实例,它们分别是byte.class, short.class, int.class, long.class, float.class, double.class, char.class, boolean.class八个基本类型的Class实例对象。还剩下一个想不到吧哈哈,还有一个是void.class你敢信?这里我是这么理解的,定义方法时是需要返回值类型的,不需要返回值时用void,那么我们是不是可以把void理解为特殊的基本类型。
这边需要注意的是:包装类类型与基本数据类型两个是不能划等号的。并且在包装类中都有一个自定义常量TYPE,用于接收对应的普通类型。API以及代码演示如下:
注意:基本数据类型没有类的概念,自然而然也没有对象,所以获取基本类型的Class实例只能通过 基本类型.class 来获取。
多说一点:JVM在创建时,是在CPU中请求一份虚拟内存,那么我们的程序都是在该内存中跑,那么要使用类相关的信息,需要类的字节码对象。所以使用普通类型应该也有自己的字节码对象。但是普通类型没有对象的概念,所以只能通过普通类型的属性来获取它的Class对象。
5.3数组的Class实例
一样的,我们平时使用的数组是引用类型,也是个对象,但是它却没有类这个概念。那么数组的Class实例该如何获取呢?直接上图,本身我也是个java小白,自学过程中感觉图片更直接,并且更容易让人接受。
数组的Class实例的表示方式有两种:方式1: 数组类型.class; 方式2: 数组对象.getClass();
前方高能!!!这里需要注意,所有的具有相同的维数和相同元素类型的数组共享同一份字节码对象,和元素没有关系.
维数就是一维数组,二维数组,指的是几维数组。元素类型是指相同的数据类型。这里来段代码,颠覆三观的那种。
数组Class实例对象总结:只要维数跟数组类型一致,它们就共用一个Class实例对象!
5.4让我恍然大悟是什么
当一个程序在运行时,该程序所用到的变量,方法,数组,类等等用到的东西都必须在JVM进程申请的虚拟空间中提前“提供”。那么基本数据类型是如何提供的呢?上文提到过,在JVM进程申请空间后就会产生九个基本数据类型的Class实例。这样就更加了解java了,能加深自己对java的理解。
6.反射获取构造器及反射创建对象
构造器最重要的作用的创建对象,我听到这句话时的第一反应,我直接new它不香吗?实际上在后面学习到框架时,在配置文件中给我们的都是字符串形式的,直接new的方式就不可行了。需要注意的有:
- 如果字节码对象中有无参构造,可以直接使用Class中的方法创建对象。
- 除了public修饰的构造方法,其他修饰符修饰的方法反射都无法直接获取,需要用到图中类的setAccessibleObect()方法,将不可见的改成可见的。
- 该类有三个直接子类,所以构造方法,成员方法,成员变量都存在暴力破解,这一点是相同的,可以比较学习。
经验:从6开始其实就是具体的API了,这一块一定要敲,只有敲几遍代码才能有印象。通过敲代码我收获了红框中的两个参数应该写什么。
7.反射获取类中方法及反射调用方法
这里和构造器是类似的,需要注意的也是暴力反射,将不可访问的,通过setAccessibleObject()方法设置成可访问的。代码一定要自己独立写一遍。独立!独立!独立!
截图截不下分成两张了
8.反射获取成员变量及成员的读写
这里跟6,7是一样的,比较学习你能收获很多。
9.新发现:反射调用静态方法和数组参数
9.1调用静态方法
静态方法不属于任何对象,静态方法属于类本身.此时把invoke方法的第一个参数设置为null即可.也就是说invoke()方法中的第一个参数可以是任意类型,一般使用null。
Person类中的静态方法test
通过反射获取静态方法,并执行
9.2调用数组参数(可变参数)
- 调用方法的时候把实际参数统统作为Object数组的元素即可.
- Method对象.invoke(方法底层所属对象,new Object[]{所有实参} );
引用类型的会被解包,而普通类型不会
10.补充:Class类中的其他API
添加这一点是为了说明,反射可以获取到一个类的所有信息!不是很重要吧,个人认为。