Java-反射机制介绍

我觉得作为一个 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package qiushao.net;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

class Person {
private String mName;
public Person() {
mName = "unknown";
}
public Person(String name) {
mName = name;
}
public String getName() {
return mName;
}
public void setName(String name) {
mName = name;
}
public void show() {
System.out.println("hello, my name is " + mName);
}
}

public class App
{
public static void main( String[] args ) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
//假设我们在编译时不能直接 import qiushao.net.Person; 这个类, 但我们知道系统有这么一个类存在,而且我们知道它的包名类名。

//1. 获取类对象
Class<?> claz = Class.forName("qiushao.net.Person");
//2. 调用无参构造函数创建类实例
Object person = claz.newInstance();

//3. 调用方法
Method setNameMethod = claz.getMethod("setName", String.class);
setNameMethod.invoke(person, "qiushao");

Method showMethod = claz.getMethod("show");
showMethod.invoke(person);

//4. 设置成员变量值
Field nameField = claz.getDeclaredField("mName");
nameField.setAccessible(true);
nameField.set(person, "foobar");

showMethod.invoke(person);
}
}

通过上面的例子,我们展示了反射的一些最基本用法,比如获取类对象,构造类实例,调用实例的方法,修改成员变量的值等。接下来我们详细的讨论反射的各种功能具体用法。

4. 获取 Class 对象

我们可以通过三种形式来获取一个类的 Class 对象:

  1. 直接通过 类名.class 的方式得到:Class<?> claz = Person.class;, 如果我们在编译时期可以 import Person 这个类的话,就可以这么用。
  2. 通过对象调用 getClass() 方法来获取: Class<?> claz = obj.getClass();, 比如你传过来一个 Object 类型的对象,而我不知道你具体是什么类,用这种方法
  3. 通过 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. 调用无参构造函数
    无参构造函数是指没有参数的构造函数,如果一个类有无参构造函数的话,我们就可以通过这种方式来创建类的实例:

    1
    2
    Class<?> claz = Class.forName("qiushao.net.Person");
    Object person = claz.newInstance();
  2. 调用指定构造函数
    如果一个类没有无参构造函数,使用第一种方法就会报错,我们需要使用另外一种方法:

    1
    2
    3
    Class<?> claz = Class.forName("qiushao.net.Person");
    Constructor constructor = claz.getConstructor(String.class);
    Object person = constructor.newInstance("qiushao");

我们可以通过 claz.getConstructor 来获取指定参数类型的构造函数, 这个例子里面就是获取有一个 String 类型参数的构造函数。
然后通过 constructor.newInstance 来创建实例,参数就是我们要传给构造函数的数据。

获取构造函数的接口还有其他几个,使用方法基本类似,就不再举例了:

1
2
3
4
5
6
7
8
9
10
// 获取“参数是parameterTypes”的public的构造函数
public Constructor getConstructor(Class[] parameterTypes)
// 获取全部的public的构造函数
public Constructor[] getConstructors()
// 获取“参数是parameterTypes”的,并且是类自身声明的构造函数,包含public、protected和private构造函数。
public Constructor getDeclaredConstructor(Class[] parameterTypes)
// 获取类自身声明的全部的构造函数,包含public、protected和private方法。
public Constructor[] getDeclaredConstructors()
// 如果这个类是“其它类的构造函数中的内部类”,调用getEnclosingConstructor()就是这个类所在的构造函数; 这种场景我还没见过,居然会在构造函数中声明一个类。
public Constructor getEnclosingConstructor()

6. 调用方法

参考上面的例子,通过反射调用类方法的基本形式为:

1
2
3
Method setNameMethod = claz.getMethod("setName", String.class);
setNameMethod.setAccessible(true);
setNameMethod.invoke(person, "qiushao");

如果要调用的方法是 private 的,则需要调用 setAccessible(true) 来修改访问权限。
如果要调用的方法是类的静态方法,则 invoke 的第一个参数传 null 即可。
获取类的方法的接口有好几个,解析如下:

1
2
3
4
5
6
7
8
9
10
// 获取“名称是name,参数是parameterTypes”的public的函数(包括从基类继承的、从接口实现的所有public函数)
public Method getMethod(String name, Class[] parameterTypes)
// 获取全部的public的函数(包括从基类继承的、从接口实现的所有public函数)
public Method[] getMethods()
// 获取“名称是name,参数是parameterTypes”,并且是类自身声明的函数,包含public、protected和private方法。
public Method getDeclaredMethod(String name, Class[] parameterTypes)
// 获取全部的类自身声明的函数,包含public、protected和private方法。
public Method[] getDeclaredMethods()
// 如果这个类是“其它类中某个方法的内部类”,调用getEnclosingMethod()就是这个类所在的方法;若不存在,返回null。
public Method getEnclosingMethod()

7. 访问变量

参考上面的例子,通过反射访问类成员变量的基本形式为:

1
2
3
4
5
6
Field nameField = claz.getDeclaredField("mName");
nameField.setAccessible(true);
//修改成员变量的值
nameField.set(person, "foobar");
//获取成员变量的值
String value = (String) nameField.get(person);

如果要访问的变量是 private 的,则需要调用 setAccessible(true) 来修改访问权限。
如果要访问的变量是类的静态变量,则 set/get 的第一个参数传 null 即可。
如果要访问的变量是基本数据类型(int, double等)的话,则要通过 setInt/getInt 等形式去设置和获取值。
获取变量的接口有好几个,解析如下:

1
2
3
4
5
6
7
8
// 获取名称是"name"的public的成员变量(包括从基类继承的、从接口实现的所有public成员变量)
public Field getField(String name)
// 获取全部的public成员变量(包括从基类继承的、从接口实现的所有public成员变量)
public Field[] getFields()
// 获取名称是"name",并且是类自身声明的成员变量,包含public、protected和private成员变量。
public Field getDeclaredField(String name)
// 获取全部的类自身声明的成员变量,包含public、protected和private成员变量。
public Field[] getDeclaredFields()

8. 获取父类信息

我们可以通过以下接口,获取父类的信息

1
2
3
4
5
6
7
8
// 获取实现的全部接口,由于编译擦除,没有显示泛型参数
Class<?>[] getInterfaces()
// 获取实现的全部接口, 包含泛型参数
public Type[] getGenericInterfaces()
// 获取直接继承的父类, 由于编译擦除,没有显示泛型参数
Class<? super T> getSuperclass();
// 获取直接继承的父类, 包含泛型参数
public Type getGenericSuperclass()

我们获取到父类的 Class 之后,就可以用我们前面学到的方法来访问父类的方法和变量了。

9. 枚举的反射

枚举本质上就是一个类,所以上面介绍的方法,对于枚举来说是同样适用的。
枚举常量我们可以理解为是类的 public static final int 变量。
下面举个枚举的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
enum COLOR {
RED,
GREEN,
BLUE
}

public class App
{
public static void showColor(COLOR color) {
System.out.println(color);
}

public static void main( String[] args ) throws ClassNotFoundException, IllegalAccessException, NoSuchMethodException, NoSuchFieldException, InvocationTargetException {
Class<?> clazApp = App.class;
Class<?> clazColor = COLOR.class;
Method showColorMethod = clazApp.getMethod("showColor", clazColor);
// 这里我们可以把 GREEN 这个枚举值当作 COLOR 枚举类的一个变量
Field greenField = clazColor.getField("GREEN");
// 枚举量可以当作是类的静态变量,所以获取它的值时,传 null 就行
Object color = greenField.get(null);
showColorMethod.invoke(null, color);
}
}