Java的十五篇:反射
音乐的浪漫之处在于:它能将封存的记忆迅速拼凑起来,你会清晰的记起当时,听这首歌的感觉和状态,就像时空真的倒回某一刻。
摇滚乐队
反射是框架设计的灵魂
(使用的前提条件:必须先得到代表的字节码的Class,Class类用于表示.class文件(字节码))
一、反射的概述
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
要想解剖一个类,必须先要获取到该类的字节码文件对象。而解剖使用的就是Class类中的方法.所以先要获取到每一个字节码文件对应的Class类型的对象.
以上的总结就是什么是反射
反射就是把java类中的各种成分映射成一个个的Java对象
例如:一个类有:成员变量、方法、构造方法、包等等信息,利用反射技术可以对一个类进行解剖,把个个组成部分映射成一个个对象。
(其实:一个类中这些成员方法、构造方法、在加入类中都有一个类来描述)
如图是类的正常加载过程:反射的原理在与class对象。
熟悉一下加载的时候:Class对象的由来是将class文件读入内存,并为之创建一个Class对象。

反方向的钟


其中这个Class对象很特殊。我们先了解一下这个Class类
Java 反射机制对于小白来说,真的是一道巨大的坎儿,其他的东西吧,无非就是内容多点,多看看多背背就好了,反射真的就是不管看了多少遍不理解就还是不理解,而且学校里面的各大教材应该都没有反射这个章节,有也是一带而过。说实话,在这篇文章之前,我对反射也并非完全了解,毕竟平常开发基本用不到,不过,看完这篇文章相信你对反射就没啥疑点了。
Class类的动态加载类
如何动态加载一个类呢?首先我们需要区分什么是动态加载?什么是静态加载?我们普遍认为编译时刻加载的类是静态加载类,运行时刻加载的类是动态加载类。我们举一个例子:
Class A{Public static void main(String[] args){if("B".equal(args[0])){B b=new B();b.start();}if("C".equal(args[0])){ C c=new C(); C.start(); } }}
上面这一段代码,当我们在用eclipse或者myeclipse的时候我们并不关心是否能够通过编译,当我们直接在cmd使用javac访问A.java类的时候,就会抛出问题:
A.java:7:错误:找不到符号B b=new B();符号: 类B位置:类AA.java:7:错误:找不到符号B b=new B();符号: 类B位置:类AA.java:12:错误:找不到符号C c=new C();符号: 类C位置:类AA.java:12:错误:找不到符号C c=new C();符号: 类C位置:类A4个错误
或许我们理所当然的认为这样应该是错,类B根本就不存在。但是如果我们多思考一下,就会发现B一定用吗?不一定。C一定用吗?也不一定。那么好,现在我们就让B类存在
Class B{Public static void start(){System.out.print("B...satrt"); }}
现在我们就先 javac B.class,让B类先开始编译。然后在运行javac A.class。结果是:
A.java:12:错误:找不到符号C c=new C();符号: 类C位置:类AA.java:12:错误:找不到符号C c=new C();符号: 类C位置:类A2个错误
我们再想,这个程序有什么问题。如果你说没有什么问题?C类本来就不存在啊!那么问题来了B类已经存在了,假设我现在就想用B,我们这个程序用得了吗?答案是肯定的,用不了。那用不了的原因是什么?因为我们这个程序是做的类的静态加载,也就是说new创建对象是静态加载类,在编译时刻就需要加载所有的,可能使用到的类。所以不管你用不用这个类。现在B类是存在的,但是我们这个程序仍然用不了,因为会一直报C类有问题,所以B类我也用不了。那么在实际应用当中,我们肯定需要如果B类存在,B类我就能用,当用C类的时候,你再告诉我错了。如果说将来你有100个类,只要其中一个类出现问题,其它99个类你都用不了。所以这并不是我们想要的。我们想要的就是我用那个类就加载那个类,也就是常说的运行时刻加载,动态加载类。如何实现动态加载类呢?我们可以建这么一个类
Class All{Public static void start(){try{Class cl= Class.forName(args[0]);//通过类类型,创建该类的对象cl.newInstance();}catch(Exception e){e.printStackTrace();}}}
前面我们在分析Class实例化对象的方式的时候,Class.forName("类的全称"),它不仅仅表示了类的类类型,还表示了动态加载类。当我们javac All.java的时候,它不会报任何错误,也就是说在编译的时候是没有错误的。只有当我们具体用某个类的时候,那个类不存在,它才会报错。如果加载的类是B类,就需要:
B bt = (B) cl.newInstance();
万一加载的是C类呢,可以改成
C ct = (C) cl.newInstance();
但是如果我想用很多的类或者加载很多的类,该怎么办?我们可以统一一个标准,不论C类还是B类或者其他的类,比如定义一个标准Stand s = (Stand) cl.newInstance();
只要B类和C类都是这个标准的就行了。
Class All{Public static void start(){try{Class cl= Class.forName(args[0]);//通过类类型,创建该类的对象Stand s = (Stand) cl.newInstance();s.start();}catch(Exception e){e.printStackTrace();}}}interface Stand {Public void start();}
现在如果我想要用B类,我们只需要:
Class B implements Stand{Public void start(){System.out.print("B...satrt");}}
加载B类,编译运行。
javac B.javajavac Stand.javajava Stand B
结果:B...satrt
如果以后想用某一个类,不需要重新编译,只需要实现这个标准的接口即可。只需要动态的加载新的东西就行了。这就是动态加载类。

1. 抛砖引玉:为什么要使用反射
前文我们说过,接口的使用提高了代码的可维护性和可扩展性,并且降低了代码的耦合度。
来看个例子:
首先,我们拥有一个接口 X 及其方法 test,和两个对应的实现类 A、B:
publicclass Test {
interface X {
public void test();
}
class A implements X{
@Override
public void test() {
System.out.println("I am A");
}
}
class B implements X{
@Override
public void test() {
System.out.println("I am B");
}
}
通常情况下,我们需要使用哪个实现类就直接 new 一个就好了,看下面这段代码:
publicclass Test {
......
public static void main(String[] args) {
X a = create1("A");
a.test();
X b = create1("B");
b.test();
}
public static X create1(String name){
if (name.equals("A")) {
returnnew A();
} elseif(name.equals("B")){
returnnew B();
}
returnnull;
}
}
按照上面这种写法,如果有成百上千个不同的 X 的实现类需要创建,那我们岂不是就需要写上千个 if 语句来返回不同的 X 对象?
我们来看看看反射机制是如何做的:
publicclass Test {
public static void main(String[] args) {
X a = create2("A");
a.test();
X b = create2("B");
b.testReflect();
}
// 使用反射机制
public static X create2(String name){
Class<?> class = Class.forName(name);
X x = (X) class.newInstance();
return x;
}
}
向 create2() 方法传入包名和类名,通过反射机制动态的加载指定的类,然后再实例化对象。
看完上面这个例子,相信诸位对反射有了一定的认识。反射拥有以下四大功能:
在运行时(动态编译)获知任意一个对象所属的类。
在运行时构造任意一个类的对象。
在运行时获知任意一个类所具有的成员变量和方法。
在运行时调用任意一个对象的方法和属性。
上述这种动态获取信息、动态调用对象的方法的功能称为 Java 语言的反射机制。
2. 理解 Class 类
要想理解反射,首先要理解 Class 类,因为 Class 类是反射实现的基础。
img
在程序运行期间,JVM 始终为所有的对象维护一个被称为运行时的类型标识,这个信息跟踪着每个对象所属的类的完整结构信息,包括包名、类名、实现的接口、拥有的方法和字段等。可以通过专门的 Java 类访问这些信息,这个类就是 Class 类。我们可以把 Class 类理解为类的类型,一个 Class 对象,称为类的类型对象,一个 Class 对象对应一个加载到 JVM 中的一个 .class 文件。
在通常情况下,一定是先有类再有对象。以下面这段代码为例,类的正常加载过程是这样的:

img
首先 JVM 会将你的代码编译成一个 .class 字节码文件,然后被类加载器(Class Loader)加载进 JVM 的内存中,同时会创建一个 Date 类的 Class 对象存到堆中(注意这个不是 new 出来的对象,而是类的类型对象)。JVM 在创建 Date 对象前,会先检查其类是否加载,寻找类对应的 Class 对象,若加载好,则为其分配内存,然后再进行初始化 new Date()。

img
需要注意的是,每个类只有一个 Class 对象,也就是说如果我们有第二条 new Date() 语句,JVM 不会再生成一个 Date 的 Class 对象,因为已经存在一个了。这也使得我们可以利用 == 运算符实现两个类对象比较的操作:

img
OK,那么在加载完一个类后,堆内存的方法区就产生了一个 Class 对象,这个对象就包含了完整的类的结构信息,我们可以通过这个 Class 对象看到类的结构,就好比一面镜子。所以我们形象的称之为:反射。
说的再详细点,再解释一下。上文说过,在通常情况下,一定是先有类再有对象,我们把这个通常情况称为 “正”。那么反射中的这个 “反” 我们就可以理解为根据对象找到对象所属的类(对象的出处)
img
通过反射,也就是调用了 getClass() 方法后,我们就获得了 Date 类对应的 Class 对象,看到了 Date 类的结构,输出了 Date 对象所属的类的完整名称,即找到了对象的出处。当然,获取 Class 对象的方式不止这一种。
img
3. 获取 Class 类对象的四种方式
从 Class 类的源码可以看出,它的构造函数是私有的,也就是说只有 JVM 可以创建 Class 类的对象,我们不能像普通类一样直接 new 一个 Class 对象。
img
我们只能通过已有的类来得到一个 Class类对象,Java 提供了四种方式:
第一种:知道具体类的情况下可以使用:
img
但是我们一般是不知道具体类的,基本都是通过遍历包下面的类来获取 Class 对象,通过此方式获取 Class 对象不会进行初始化。
第二种:通过 **Class.forName()**传入全类名获取:

这个方法内部实际调用的是 forName0:

img
第 2 个 boolean 参数表示类是否需要初始化,默认是需要初始化。一旦初始化,就会触发目标对象的 static 块代码执行,static 参数也会被再次初始化。
第三种:通过对象实例 instance.getClass() 获取:

img
第四种:通过类加载器 xxxClassLoader.loadClass() 传入类路径获取

img
通过类加载器获取 Class 对象不会进行初始化,意味着不进行包括初始化等一些列步骤,静态块和静态对象不会得到执行。这里可以和 forName 做个对比。

4. 通过反射构造一个类的实例
上面我们介绍了获取 Class 类对象的方式,那么成功获取之后,我们就需要构造对应类的实例。下面介绍三种方法,第一种最为常见,最后一种大家稍作了解即可。
① 使用 Class.newInstance
举个例子:

img
创建了一个与 alunbarClass2 具有相同类类型的实例。
需要注意的是,*newInstance***方法调用默认的构造函数(无参构造函数)初始化新创建的对象。如果这个类没有默认的构造函数, 就会抛出一个异常。

img
② 通过反射先获取构造方法再调用
由于不是所有的类都有无参构造函数又或者类构造器是 private 的,在这样的情况下,如果我们还想通过反射来实例化对象,Class.newInstance 是无法满足的。
此时,我们可以使用 Constructor 的 newInstance 方法来实现,先获取构造函数,再执行构造函数。

img
从上面代码很容易看出,Constructor.newInstance 是可以携带参数的,而 Class.newInstance 是无参的,这也就是为什么它只能调用无参构造函数的原因了。
大家不要把这两个 newInstance 方法弄混了。如果被调用的类的构造函数为默认的构造函数,采用Class.newInstance() 是比较好的选择, 一句代码就 OK;如果需要调用类的带参构造函数、私有构造函数等, 就需要采用 Constractor.newInstance()
批量获取构造函数:
1)获取所有"公有的"构造方法
img
2)获取所有的构造方法(包括私有、受保护、默认、公有)
img
单个获取构造函数:
1)获取一个指定参数类型的"公有的"构造方法
img
2)获取一个指定参数类型的"构造方法",可以是私有的,或受保护、默认、公有
img
举个例子:
package fanshe;
publicclass Student {
//(默认的构造方法)
Student(String str){
System.out.println("(默认)的构造方法 s = " + str);
}
// 无参构造方法
public Student(){
System.out.println("调用了公有、无参构造方法执行了。。。");
}
// 有一个参数的构造方法
public Student(char name){
System.out.println("姓名:" + name);
}
// 有多个参数的构造方法
public Student(String name ,int age){
System.out.println("姓名:"+name+"年龄:"+ age);//这的执行效率有问题,以后解决。
}
// 受保护的构造方法
protected Student(boolean n){
System.out.println("受保护的构造方法 n = " + n);
}
// 私有构造方法
private Student(int age){
System.out.println("私有的构造方法年龄:"+ age);
}
}
----------------------------------
publicclass Constructors {
public static void main(String[] args) throws Exception {
// 加载Class对象
Class clazz = Class.forName("fanshe.Student");
// 获取所有公有构造方法
Constructor[] conArray = clazz.getConstructors();
for(Constructor c : conArray){
System.out.println(c);
}
// 获取所有的构造方法(包括:私有、受保护、默认、公有)
conArray = clazz.getDeclaredConstructors();
for(Constructor c : conArray){
System.out.println(c);
}
// 获取公有、无参的构造方法
// 因为是无参的构造方法所以类型是一个null,不写也可以:这里需要的是一个参数的类型,切记是类型
// 返回的是描述这个无参构造函数的类对象。
Constructor con = clazz.getConstructor(null);
Object obj = con.newInstance(); // 调用构造方法
// 获取私有构造方法
con = clazz.getDeclaredConstructor(int.class);
System.out.println(con);
con.setAccessible(true); // 为了调用 private 方法/域 我们需要取消安全检查
obj = con.newInstance(12); // 调用构造方法
}
}
③ 使用开源库 Objenesis
Objenesis 是一个开源库,和上述第二种方法一样,可以调用任意的构造函数,不过封装的比较简洁:
publicclass Test {
// 不存在无参构造函数
privateint i;
public Test(int i){
this.i = i;
}
public void show(){
System.out.println("test..." + i);
}
}
------------------------
public static void main(String[] args) {
Objenesis objenesis = new ObjenesisStd(true);
Test test = objenesis.newInstance(Test.class);
test.show();
}
使用非常简单,Objenesis 由子类 ObjenesisObjenesisStd实现。详细源码此处就不深究了,了解即可。
5. 通过反射获取成员变量并使用
和获取构造函数差不多,获取成员变量也分批量获取和单个获取。返回值通过 Field 类型来接收。
批量获取:
1)获取所有公有的字段
img
2)获取所有的字段(包括私有、受保护、默认的)

img
单个获取:
1)获取一个指定名称的公有的字段

img
2)获取一个指定名称的字段,可以是私有、受保护、默认的

img
获取到成员变量之后,如何修改它们的值呢?

img
invoke 方法中包含两个参数:
obj:哪个对象要来调用这个方法
args:调用方法时所传递的实参
举个例子:
package fanshe.method;
publicclass Student {
public void show1(String s){
System.out.println("调用了:公有的,String参数的show1(): s = " + s);
}
protected void show2(){
System.out.println("调用了:受保护的,无参的show2()");
}
void show3(){
System.out.println("调用了:默认的,无参的show3()");
}
private String show4(int age){
System.out.println("调用了,私有的,并且有返回值的,int参数的show4(): age = " + age);
return"abcd";
}
}
-------------------------------------------
publicclass MethodClass {
public static void main(String[] args) throws Exception {
// 获取 Class对象
Class stuClass = Class.forName("fanshe.method.Student");
// 获取公有的无参构造函数
Constructor con = stuClass.getConstructor();
// 获取所有公有方法
stuClass.getMethods();
Method[] methodArray = stuClass.getMethods();
for(Method m : methodArray){
System.out.println(m);
}
// 获取所有的方法,包括私有的
methodArray = stuClass.getDeclaredMethods();
for(Method m : methodArray){
System.out.println(m);
}
// 获取公有的show1()方法
Method m = stuClass.getMethod("show1", String.class);
System.out.println(m);
Object obj = con.newInstance(); // 调用构造函数,实例化一个 Student 对象
m.invoke(obj, "小牛肉");
// 获取私有的show4()方法
m = stuClass.getDeclaredMethod("show4", int.class);
m.setAccessible(true); // 解除私有限定
Object result = m.invoke(obj, 20);
System.out.println("返回值:" + result);
}
}
7. 反射机制优缺点
优点:比较灵活,能够在运行时动态获取类的实例。
缺点:
1)性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的 Java 代码要慢很多。
2)安全问题:反射机制破坏了封装性,因为通过反射可以获取并调用类的私有方法和字段。

8. 反射的经典应用场景
反射在我们实际编程中其实并不会直接大量的使用,但是实际上有很多设计都与反射机制有关,比如:
动态代理机制
使用 JDBC 连接数据库
Spring / Hibernate 框架(实际上是因为使用了动态代理,所以才和反射机制有关)
为什么说动态代理使用了反射机制,下篇文章会给出详细解释。
JDBC 连接数据库
在 JDBC 的操作中,如果要想进行数据库的连接,则必须按照以下几步完成:
通过 Class.forName() 加载数据库的驱动程序 (通过反射加载)
通过 DriverManager 类连接数据库,参数包含数据库的连接地址、用户名、密码
通过 Connection 接口接收连接
关闭连接
public static void main(String[] args) throws Exception {
Connection con = null; // 数据库的连接对象
// 1. 通过反射加载驱动程序
Class.forName("com.mysql.jdbc.Driver");
// 2. 连接数据库
con = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/test","root","root");
// 3. 关闭数据库连接
con.close();
}
Spring 框架
反射机制是 Java 框架设计的灵魂,框架的内部都已经封装好了,我们自己基本用不着写。典型的除了Hibernate 之外,还有 Spring 也用到了很多反射机制,最典型的就是 Spring 通过 xml 配置文件装载 Bean(创建对象),也就是 Spring 的 IoC,过程如下:
加载配置文件,获取 Spring 容器
使用反射机制,根据传入的字符串获得某个类的 Class 实例
// 获取 Spring 的 IoC 容器,并根据 id 获取对象
public static void main(String[] args) {
// 1.使用 ApplicationContext 接口加载配置文件,获取 spring 容器
ApplicationContext ac = new ClassPathXmlApplicationContext("spring.xml");
// 2. 使用反射机制,根据这个字符串获得某个类的 Class 实例
IAccountService aService = (IAccountService) ac.getBean("accountServiceImpl");
System.out.println(aService);
}
另外,Spring AOP 由于使用了动态代理,所以也使用了反射机制,这点我会在 Spring 的系列文章中详细解释。
jdk 的class
所以拿到这个类后,就相当于拿到了咱们想解剖的类,那怎么拿到这个类?
看API文档后,有一个方法forName(String className); 而且是一个静态的方法,这样咱们就可以得到想反射的类了
到这里,看Class clazz = Class.forName("com.cj.test.Person");这个应该有点感觉了吧
Class.forName("com.cj.test.Person");因为这个方法里接收的是个字符串,字符串的话,我们就可以写在配置文件里,然后利用反射生成我们需要的对象,这才是我们想要的。很多框架里都有类似的配置
扩展:
1、除了上述的Spring配置文件里会用到反射生成bean对象,其他常见的MVC框架,比如Struts2、SpringMVC等等一些框架里还有很多地方都会用到反射。
前端夜页面录入的一些信息通过表单或者其他形式传入后端,后端框架就可以利用反射生成对应的对象,并利用反射操作它的set、get方法把前端传来的信息封装到对象里。
2、框架的代码里经常需要利用反射来操作对象的set、get方法,来把程序的数据封装到Java对象中去。
如果每次都使用反射来操作对象的set、get方法进行设置值和取值的话,过于麻烦,所以JDK里提供了一套API,专门用于操作Java对象的属性(set/get方法),这就是内省
3、平常用到的框架,除了配置文件的形式,现在很多都使用了注解的形式。
其实注解也和反射息息相关:使用反射也能轻而易举的拿到类、字段、方法上的注解,然后编写注解解析器对这些注解进行解析,做一些相关的处理
点个在看你最好看
总结:
1.反射拥有以下四大功能:
在运行时(动态编译)获知任意一个对象所属的类。
在运行时构造任意一个类的对象。
在运行时获知任意一个类所具有的成员变量和方法。
在运行时调用任意一个对象的方法和属性。
上述这种动态获取信息、动态调用对象的方法的功能称为 Java 语言的反射机制。
反射的具体原理:
在通常情况下,一定是先有类然后再 new 一个对象出来的对吧,类的正常加载过程是这样的:
首先 JVM 会将我们的代码编译成一个 .class 字节码文件,然后被类加载器(ClassLoader)加载进 JVM 的内存中,同时会创建这个类的 Class 对象存到堆中(注意这个不是 new 出来的对象,而是类的类型对象)。JVM 在创建这个类对象前,会先检查其类是否加载,寻找类对应的 Class 对象,若加载好,则为其分配内存,然后再进行初始化 new 操作。
OK,那么在加载完一个类后,堆内存的方法区就产生了一个 Class 对象,并且包含了这个类的完整结构信息,我们可以通过这个 Class 对象看到类的结构,就好比一面镜子。所以我们形象的称之为:反射。
2.优点:比较灵活,能够在运行时动态获取类的实例。
缺点:
1)性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的 Java 代码要慢很多。
2)安全问题:反射机制破坏了封装性,因为通过反射可以获取并调用类的私有方法和字段。
反射在我们实际编程中其实并不会直接大量的使用,但是实际上有很多设计都与反射机制有关,比如:
动态代理机制
使用 JDBC 连接数据库
Spring / Hibernate 框架(实际上是因为使用了动态代理,所以才和反射机制有关,这个地方可以酌情扩展)
3.通过反射我们可以获取到私有方法里面的属性,也可以通过反射扩展或者节省我们的代码,让我们的代码更加优雅!
4.通过反射,也就是调用了 getClass() 方法后,我们就获得了这个类类对应的 Class 对象,看到了这个类的结构,输出了类对象所属的类的完整名称,即找到了对象的出处。
5.知道了动态加载类,通过反射实现了*动态加载类
