我觉得作为一个 java 语言的使用者,无论你是 web 开发,还是 Android 开发,你都有必要了解一下这几个知识点:反射,类的加载机制,注解,动态代理。作为一个普通的开发者,你可能没有意识到你有使用过这些技术,但实际上这些技术是应用非常广的,基本上我们使用的各种 web 框架, ORM 框架,插件化框架都会依赖这些技术去实现。如果你想对使用的各种框架有更深入的理解,或者甚至想开发自己的框架,那你一定要深入理解前面提到的几个基本知识点。这里先介绍反射,后面再依次介绍其他知识点。
1. 什么是反射
反射机制是 Java 语言的一个重要特性。在学习 Java 反射机制前,大家应该先分清楚两个概念: 编译期和运行期。
- 编译期:是指把源码交给编译器编译成计算机可以执行的文件的过程。在 Java 中也就是把 Java 代码编成 class 文件的过程。编译期只是做了一些翻译功能,并没有把代码放在内存中运行起来,而只是把代码当成文本进行操作,比如检查错误。
- 运行期:是把编译后的文件交给计算机执行,直到程序运行结束。所谓运行期就把在磁盘中的代码放到内存中执行起来。
Java 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为 Java 语言的反射机制。简单来说,反射机制指的是程序在运行时能够获取自身的信息。在 Java 中,只要给定类的名字,就可以通过反射机制来获得类的所有信息,而这个类在编译过程中甚至是还未存在的。在运行的时候我们可以通过配置文件获取某个类的类名,然后使用反射机制构造这个类的对象,调用这个对象的方法,修改这个对象的成员变量。
2. 反射的使用场景
Java 反射机制在 web 开发框架, ORM 框架, 插件化开发等场景中得到了广泛运用。
比如说 web 开发框架 Spring 中,最重要的概念就是 IOC 控制反转。而 IOC 的实现原理就是反射。通过反射来构造 Java Bean 的对象,调用其方法。
比如说 Android 开发中常用的 ORM 框架: GreenDao, LiteOrm 等, 也是通过反射来读写 Java Bean 对象的成员变量的。
如果你只是使用这些框架,你可能感觉不到反射的存在,实际上反射却是无处不在。
3. 反射的基本用法
我们先写个简单的 Demo,来感性的认识一下反射的基本用法:
1 | package qiushao.net; |
通过上面的例子,我们展示了反射的一些最基本用法,比如获取类对象,构造类实例,调用实例的方法,修改成员变量的值等。接下来我们详细的讨论反射的各种功能具体用法。
4. 获取 Class 对象
我们可以通过三种形式来获取一个类的 Class 对象:
- 直接通过 类名.class 的方式得到:
Class<?> claz = Person.class;
, 如果我们在编译时期可以 import Person 这个类的话,就可以这么用。 - 通过对象调用 getClass() 方法来获取:
Class<?> claz = obj.getClass();
, 比如你传过来一个 Object 类型的对象,而我不知道你具体是什么类,用这种方法 - 通过 Class 对象的 forName() 静态方法来获取:
Class<?> claz = Class.forName("qiushao.net.Person");
, 不能在编译时 import, 也不能在运行时获取类对象的话,只能用这种方法了。这种用法是最常用的了。forName 还有另外一个重载方法Class<?> forName(String name, boolean initialize, ClassLoader loader)
, 可以传入一个 ClassLoader 对象来指定类的加载方法。这个就涉及到类的加载机制了,这里先不讨论。
获取到 Class 之后,我们可以进而可以通过 claz 获取一些这个类的基本信息:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 获取“类名”, 值为 Person
public String getSimpleName()
// 获取“完整类名”, 值为 qiushao.net.Person
public String getName()
// 类是不是“枚举类”
public boolean isEnum()
// obj是不是类的对象
public boolean isInstance(Object obj)
// 类是不是“接口”
public boolean isInterface()
// 类是不是“本地类”。本地类,就是定义在方法内部的类。
public boolean isLocalClass()
// 类是不是“成员类”。成员类,是内部类的一种, 其定义请参考 https://blog.csdn.net/a327369238/article/details/52780442
public boolean isMemberClass()
// 类是不是“基本类型”。 基本类型,包括void和boolean、byte、char、short、int、long、float 和 double这几种类型。
public boolean isPrimitive()
5. 创建类实例
通过反射来创建类实例主要有两种方式:
调用无参构造函数
无参构造函数是指没有参数的构造函数,如果一个类有无参构造函数的话,我们就可以通过这种方式来创建类的实例:1
2Class<?> claz = Class.forName("qiushao.net.Person");
Object person = claz.newInstance();调用指定构造函数
如果一个类没有无参构造函数,使用第一种方法就会报错,我们需要使用另外一种方法:1
2
3Class<?> claz = Class.forName("qiushao.net.Person");
Constructor constructor = claz.getConstructor(String.class);
Object person = constructor.newInstance("qiushao");
我们可以通过 claz.getConstructor 来获取指定参数类型的构造函数, 这个例子里面就是获取有一个 String 类型参数的构造函数。
然后通过 constructor.newInstance 来创建实例,参数就是我们要传给构造函数的数据。
获取构造函数的接口还有其他几个,使用方法基本类似,就不再举例了:
1 | // 获取“参数是parameterTypes”的public的构造函数 |
6. 调用方法
参考上面的例子,通过反射调用类方法的基本形式为:
1 | Method setNameMethod = claz.getMethod("setName", String.class); |
如果要调用的方法是 private 的,则需要调用 setAccessible(true)
来修改访问权限。
如果要调用的方法是类的静态方法,则 invoke 的第一个参数传 null 即可。
获取类的方法的接口有好几个,解析如下:
1 | // 获取“名称是name,参数是parameterTypes”的public的函数(包括从基类继承的、从接口实现的所有public函数) |
7. 访问变量
参考上面的例子,通过反射访问类成员变量的基本形式为:
1 | Field nameField = claz.getDeclaredField("mName"); |
如果要访问的变量是 private 的,则需要调用 setAccessible(true)
来修改访问权限。
如果要访问的变量是类的静态变量,则 set/get 的第一个参数传 null 即可。
如果要访问的变量是基本数据类型(int, double等)的话,则要通过 setInt/getInt 等形式去设置和获取值。
获取变量的接口有好几个,解析如下:
1 | // 获取名称是"name"的public的成员变量(包括从基类继承的、从接口实现的所有public成员变量) |
8. 获取父类信息
我们可以通过以下接口,获取父类的信息
1 | // 获取实现的全部接口,由于编译擦除,没有显示泛型参数 |
我们获取到父类的 Class 之后,就可以用我们前面学到的方法来访问父类的方法和变量了。
9. 枚举的反射
枚举本质上就是一个类,所以上面介绍的方法,对于枚举来说是同样适用的。
枚举常量我们可以理解为是类的 public static final int 变量。
下面举个枚举的例子:
1 | enum COLOR { |