1、引言
反射的重点在于这个“反”字,理解反射,重点要在于这个“反”的含义。既然是“反”,那么一定是与通常不一致的情况。那我们首先来看下正常情况下,Java程序从编写到运行的一个过程。
在Java中,万物皆是类。开发人员在编写Java代码时,就是一个创建并编写大量Java类的过程。Java程序运行时,ClassLoader就会将所有用到的Java类加载到JVM虚拟机中。如下是一个简单的Java示例代码,代码中定义了一个Person类,以及一个包含main函数作为程序入口的Main类 。在main函数里面,代码引用了Person类。
class Person {
private String name;
public Person() {
}
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
class Main {
public static void main(String[] args) {
Person person = new Person("Joy");
System.out.println(person.getName());
}
}
这个小程序,从开发者层面来看,在开发这段小程序时,开发人员一定是知道main程序中依赖的Person类的详细信息,包括Person类的属性、方法等,所以开发人员可以在main函数中明确的使用Person这个类。
从代码的编译层面来看,在这个小程序的编译阶段,编译Main这个类时,编译器发现了对Person类的引用,那么编译器就一定会去找Person类的相关信息,然后将Person类进行编译,以便在Main类中可以正常使用。编译器如果没有找到Person类,那么就一定会编译失败。换言之,这个小程序编译成功了,那么就说明在编译阶段,编译器已经获取到了所有Person类的详细信息。
从代码的运行程序面来看,程序运行时,ClassLoader将需要用到的类都加载到了JVM中,所有JVM中也一定是清楚的知道相关类的所有信息的。
如下图所示,对于一个类来说,从代码编写阶段到程序运行阶段,主要有以下几个状态:
首先一个类对应的是*.java文件中的一个class,java文件中定义了类的各种属性、方法等。经过编译之后,每个类对应一个class文件,class文件中也描述了类的所有相关信息。程序运行时,类加载器对class文件进行解析,加载对应的类,同时会在JVM中生成一个Class对象,每个Class对象和每个class文件是一一对应的,也就是每个类在JVM中都有且只有一个对应的Class对象存在。这个Class对象描述了其对应的java类的详细信息。当要实例化这个类的一个对象时,就是根据这个类对应的Class对象获取类的相关信息,并根据此信息生成该类的一个新的对象。
以上就是java程序中,在通常情况下运行时,类从代码阶段到执行阶段所有的几个简单状态。
2、什么是反射
说完通常情况了,就该说一下特殊情况了。假设有这么一个需求,要求编写一段代码,这个代码的作用是,对于给定的任意一个类,要求调用这个类的任意某个方法。
这个要怎么做呢?
和上面的不同点在于,在开发人员层面,编写上面示例代码时,开发者是明确知道Person类的,知道Person类的所有信息,也知道要调用Person类的某个具体的方法。但是现在这个需求,开发人员什么都不知道了,不知道到底要调用类的哪个方法了,甚至不知道到底要调用哪个类的方法,对于这个类的信息是一无所知,只知道在程序运行期间会给你一个类。
从编译层面来看,在编译阶段,是没有这个类的信息的,编译器也是对这个类一无所知的。这个类的信息是在运行阶段才会有的。
那么就没有办法完成了吗?办法当然是有的,就算是开发人员在开发时不知道这个类是啥,就算是编译器在编译阶段不知道这个类的信息,但是在程序的运行阶段,是可以想办法获取这个类的信息的。只要在程序的运行阶段,我们获取了这个类的所有信息,那么我们想干啥就干啥,想执行它的啥方法就可以执行它的啥方法了。这就是反射。换言之,反射就是在程序的运行期动态获取类的信息。
如下图所示,虽然在代码开发和编译阶段可能不知道某个类的详细信息,但是在运行阶段,当这个类被加载到JVM中了就会生成一个Class对象,我们通过这个Class对象,在运行期一样可以获得这个类对应的信息,这就是反射。
这里可能就有疑问了,JVM中的Class对象是由class文件加载后生成的,为什么一定要等到加载之后才能获取类的信息呢?加载之前,从class文件中不也能够获取到这个类的信息吗?
的确,如果在程序运行之前,就拥有这个类的class文件,比如是开发人员自己写的代码编译出的class文件,或者是引用别人的jar包里面的class文件,都是可以提前获取到类的信息的,也就不用反射了。但问题是,在程序运行之前,并不是都有对应的class文件的,可能这个class文件是程序在运行时才从网络中获取到的字节流呢?甚至这个class文件就是在程序运行期才动态生成的字节流呢?这些情况下提前是不可能获知到类的相关信息的,就只能通过反射在运行期动态获取类的信息了。
下面就是一个反射的小例子,实现了上面所说的功能:
对于给定的任意一个类,要求调用这个类的任意某个方法。
import java.lang.reflect.Method;
class Person {
private String name;
public Person() {
}
public Person(String name) {
this.name = name;
}
public String getName() {
System.out.println("==========get name executed!==========");
return name;
}
public void setName(String name) {
this.name = name;
}
}
class Main {
public static void main(String[] args) throws Exception {
String className = args[0];
String methodName = args[1];
Class cls = Class.forName(className);
Method m = cls.getMethod(methodName);
Object o = cls.newInstance();
m.invoke(o);
}
}
程序的执行结果:程序运行时,指定执行Person类的getName方法。
3、反射的用途
通过前面的叙述,就算是开发人员在开发代码时可以不知道某些类的相关信息,编译器在编译阶段也可以不知道类的相关信息,但是通过在运行期动态获取类的相关信息,依然可以完成许多事情。许多的Java框架比如Spring、mybatis等等,都是基于反射实现的。
总之,最重要的就是要理解一点,对于java类,通常情况下是开发人员自己编写的java文件通过编译得到对应的class文件,或者直接引用别人的jar包得到对应的class文件,在这些方式里面,程序在运行之前就已经获取到类对应的信息了,就无需用到反射。
而反射的作用场景就在于,在程序运行之前,是获取不到对应的class文件的。这些class文件有可能是程序运行中从其他地方获取到的,有可能是动态生成的。在这种情况下,就需要通过反射在运行期来动态获取对应类的相关信息。
4、再次理解Class类
在Java语言当中,一切都是类与对象。
比如说有三辆具体的车,每一辆车都有各自的属性(比如颜色)和相同方法(比如前进),我们可以抽象出来车这样一个类,三辆车中的每一辆都是车这个类的一个对象(实例)。
在生活中,既可以有车这个类,还可以有人这个类,也可以有树这个类,总之,我们可以有无穷多个类。那么其实我们可以站在更高的一个角度来看待问题,把各个类都抽象出来形成一个新的类别,这个类别名字就叫作“类”(Class),就像我们可以把几辆车抽象出来形成的一个类叫“车”一样。车这个类是抽象出来的新的“类”的一个实例,人这个类也是抽象出来的新的这个“类”的一个实例。
为了形象化一点,我们可以这样假想以便于理解:对于每一个类,比如车这个类Car,会有对应的Car.class,我们具有无数个类也就具有无数个class文件。想象一下其实每个class文件都是一个对象,一个叫做“类”的这个类的对象。
总而言之:车对应的类型是Car,可以有无数辆具体的车,但只会有一个Car类别;类对应的类型是Class类,可以有无数个具体的类(对应无数个class文件),但只会有一个Class类别。