反射 Reflection

Java 的反射是指程序在运行期可以拿到一个对象的所有信息。正常情况下,如果我们要调用一个对象的方法,或者访问一个对象的字段,通常会传入对象实例。反射是为了解决在运行期,对某个实例一无所知的情况下,如何调用其方法。

除了 int 等基本类型外,Java 的其他类型全部都是 class(包括 interface)。例如:StringObjectRunnableException 等等。

我们可以得出结论:class(包括 interface)的本质是数据类型 Type。无继承关系的数据类型无法赋值。

1
2
3
// 无继承关系的数据类型无法赋值
Number n = new Double(123.456); // OK
String s = new Double(123.456); // compile error!

JVM 持有的每个 Class 实例都指向一个数据类型(classinterface):

image.png

一个 Class 实例包含了该 class 的所有完整信息:image.png

由于 JVM 为每个加载的 class 创建了对应的 Class 实例,并在实例中保存了该 class 的所有信息,包括类名、包名、父类、实现的接口、所有方法、字段等。因此,如果获取了某个 Class 实例,我们就可以通过这个 Class 实例获取到该实例对应的 class 的所有信息。

这种通过 Class 实例获取 class 信息的方法称为反射(Reflection)。

动态加载

JVM 在执行 Java 程序的时候,并不是一次性把所有用到的 class 全部加载到内存,而是第一次需要用到 class 时才加载。动态加载 class 的特性对于 Java 程序非常重要。利用 JVM 动态加载 class 的特性,我们才能在运行期根据条件加载不同的实现类。

访问字段

本文案例使用的实体类 Student 详见文末。

Class 类提供了以下几个方法来获取字段:

  • Field getField(name):根据字段名获取某个 public 的 field(包括父类)
  • Field getDeclaredField(name):根据字段名获取当前类的某个 field(不包括父类)
  • Field[] getFields():获取所有 public 的 field(包括父类)
  • Field[] getDeclaredFields():获取当前类的所有 field(不包括父类)

一个 Field 对象包含了一个字段的所有信息:

  • getName():返回字段名称,例如,“name”;
  • getType():返回字段类型,也是一个 Class 实例,例如,String.class
  • getModifiers():返回字段的修饰符,它是一个 int,不同的 bit 表示不同的含义。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package top.uuanqin;

import java.lang.reflect.Field;

public class Main {
public static void main(String[] args) throws Exception {
Student student = new Student("Lingling",775);
student.sayHello();
dynamicFieldSet(student,"name","LiHua");
student.sayHello();
}

public static void dynamicFieldSet(Object obj, String fieldName, Object value) throws Exception {
//取得所有public类型的属性
Field field = obj.getClass().getField(fieldName);
//直接设置属性值
field.set(obj, value);
}
}

输出:

1
2
hello my name is Lingling
hello my name is LiHua

注意,这种方法设置属性值时对应属性不能是 private。获取 private 修饰的变量时,可以先 field.setAccessible(true),作用是不管这个字段是不是 public,一律允许访问 [1]

如果使用反射可以获取 private 字段的值,那么类的封装还有什么意义?

正常情况下,我们总是通过 p.name 来访问 Personname 字段,编译器会根据 public、protected 和 private 决定是否允许访问字段,这样就达到了数据封装的目的。

而反射是一种非常规的用法,使用反射,首先代码非常繁琐,其次,它更多地是给工具或者底层框架来使用,目的是在不知道目标实例任何信息的情况下,获取特定字段的值。

获取字段的值示例:

1
2
3
4
5
6
 public static void main(String[] args) throws Exception {
Object student = new Student("李华",333);
Field field = student.getClass().getField("name");
Object value = field.get(student);
System.out.println(value);
}

调用方法

Class 类提供了以下几个方法来获取 Method:

  • Method getMethod(name, Class...):获取某个 publicMethod(包括父类)
  • Method getDeclaredMethod(name, Class...):获取当前类的某个 Method(不包括父类)
  • Method[] getMethods():获取所有 publicMethod(包括父类)
  • Method[] getDeclaredMethods():获取当前类的所有 Method(不包括父类)

一个 Method 对象包含一个方法的所有信息:

  • getName():返回方法名称,例如:"getScore"
  • getReturnType():返回方法返回值类型,也是一个 Class 实例,例如:String.class
  • getParameterTypes():返回方法的参数类型,是一个 Class 数组,例如:{String.class, int.class}
  • getModifiers():返回方法的修饰符,它是一个 int,不同的 bit 表示不同的含义。

Method 实例调用 invoke 就相当于调用该方法,invoke 的第一个参数是对象实例,即在哪个实例上调用该方法,后面的可变参数要与方法参数一致,否则将报错。

如果获取到的 Method 表示一个静态方法,调用静态方法时,由于无需指定实例对象,所以 invoke 方法传入的第一个参数永远为 null

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
package top.uuanqin;

import java.lang.reflect.Method;

public class Main {
public static void main(String[] args) throws Exception {
Student student = new Student("Daming",4456);
dynamicInvokeMethodWithNoParams(student,"sayHello");
dynamicInvokeMethodWithParams(student,"sayLocation","London",557);
}

public static Object dynamicInvokeMethodWithNoParams(Object obj, String methodName) throws Exception {
Method method = obj.getClass().getMethod(methodName);
return method.invoke(obj);
}

public static Object dynamicInvokeMethodWithParams(Object obj, String methodName, Object... values) throws Exception {
Class[] classes = new Class[values.length];
for (int i = 0; i < values.length; i++) {
// 参数包含原始数据类型的需要进行调整
if (values[i] instanceof Integer) {
// int 对应的类 int.class 或者 Integer.Type
classes[i] = Integer.TYPE; // int.class
} else {
classes[i] = values[i].getClass();
}
}
Method method = obj.getClass().getMethod(methodName,classes);
return method.invoke(obj,values);
}
}

输出:

1
2
hello my name is Daming
I am in London 557

对于非 public 方法,我们虽然可以通过 Class.getDeclaredMethod() 获取该方法实例,但直接对其调用将得到一个 IllegalAccessException。为了调用非 public 方法,我们通过 Method.setAccessible(true) 允许其调用 [1:1]

使用反射调用方法时,仍然遵循多态原则:即总是调用实际类型(如果存在)的覆写方法。

构造一个类(调用构造方法)

获取一个 classClass 实例:

1
2
3
4
5
6
7
8
9
// 方法一:直接通过一个class的静态变量class获取
Class cls = String.class;

// 方法二:如果我们有一个实例变量,可以通过该实例变量提供的getClass()方法获取
String s = "Hello";
Class cls = s.getClass();

// 方法三:如果知道一个class的完整类名,可以通过静态方法Class.forName()获取
Class cls = Class.forName("java.lang.String");

因为 Class 实例在 JVM 中是唯一的,所以,上述方法获取的 Class 实例是同一个实例。可以用 == 比较两个 Class 实例。

instanceof 不但匹配指定类型,还匹配指定类型的子类。而用 == 判断 class 实例可以精确地判断数据类型,但不能作子类型比较。

JVM 为每一种基本类型,如 int,也创建了 Class 实例,通过 int.class 访问。

如果获取到了一个 Class 实例,我们就可以通过该 Class 实例来创建对应类型的实例:

1
2
3
4
5
// 获取String的Class实例:
Class cls = String.class;
// 创建一个String实例:
String s = (String) cls.newInstance();
// newInstance() 局限是,它只能调用该类的public无参数构造方法。

为了调用任意的构造方法,Java 的反射 API 提供了 Constructor 对象,它包含一个构造方法的所有信息,可以创建一个实例。Constructor 对象和 Method 非常类似,不同之处仅在于它是一个构造方法,并且,调用结果总是返回实例。

通过 Class 实例获取 Constructor 的方法如下:

  • getConstructor(Class...):获取某个 publicConstructor
  • getDeclaredConstructor(Class...):获取某个 Constructor
  • getConstructors():获取所有 publicConstructor
  • getDeclaredConstructors():获取所有 Constructor

注意 Constructor 总是当前类定义的构造方法,和父类无关,因此不存在多态的问题。

调用非 publicConstructor 时,必须首先通过 setAccessible(true) 设置允许访问,但可能会失败 [1:2]

无参数构造方法演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Main {
public static void main(String[] args) throws Exception {
String className = "top.uuanqin.Student";

// 无参数构造方法
System.out.println("**** 无参数构造方法演示 ****");
Class clazz = Class.forName(className);
Object obj = clazz.newInstance();
// 或者:Object obj = clazz.getConstructor(new Class[0]).newInstance();
Student student = (Student) obj;
System.out.println("obj.class=" + obj.getClass().toString());
System.out.println("student.class=" + student.getClass().toString());
}
}

有参数构造方法演示:

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
package top.uuanqin;

public class Main {
public static void main(String[] args) throws Exception {
String className = "top.uuanqin.Student";
// 有参数构造方法1
System.out.println("**** 有参数构造方法1演示 ****");
Class clazz = Class.forName(className);
Class[] classes = new Class[2];
classes[0] = String.class;
classes[1] = Integer.class;
// 寻找Student的带有参数的构造函数
// 且构造函数的第一个是参数String,第二个是Integer。
// .newInstance("tom", 123): 用"tom", 123 实例化
Object obj = clazz.getConstructor(classes).newInstance("tom", 123);
Student student = (Student) obj;
System.out.println("obj.class=" + obj.getClass().toString());
System.out.println("student.class=" + student.getClass().toString());
System.out.println("Student Name: " + student.getName());
System.out.println("Student Id: " + student.getId());


// 有参数构造方法2(就是把上面的方法写成更加通用的方法)
System.out.println("**** 有参数构造方法2演示 ****");
Object obj = createObjectUsingParams(className, "Alice", 456);
Student student = (Student) obj;
System.out.println("obj.class=" + obj.getClass().toString());
System.out.println("student.class=" + student.getClass().toString());
System.out.println("Student Name: " + student.getName());
System.out.println("Student Id: " + student.getId());
}

public static Object createObjectUsingParams(String className, Object... values) throws Exception {
Class clazz = Class.forName(className);
Class[] classes = new Class[values.length];
for (int i = 0; i < classes.length; i++) {
classes[i] = values[i].getClass();
}
Object obj = clazz.getConstructor(classes).newInstance(values);
return obj;
}
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
**** 无参数构造方法演示 ****
obj.class=class top.uuanqin.Student
student.class=class top.uuanqin.Student
**** 有参数构造方法1演示 ****
obj.class=class top.uuanqin.Student
student.class=class top.uuanqin.Student
Student Name: tom
Student Id: 123
**** 有参数构造方法2演示 ****
obj.class=class top.uuanqin.Student
student.class=class top.uuanqin.Student
Student Name: Alice
Student Id: 456

获取继承关系

获取类的继承和实现关系:

  • 获取父类的 ClassgetSuperclass()。返回类型 Class
  • 获取 interfacegetInterfaces()。由于一个类可能实现一个或多个接口,返回类型 Class[]。注意,只返回当前类直接实现的接口类型,并不包括其父类实现的接口类型。

instanceof 操作符判断一个实例是否是某个类型。

如果是两个 Class 实例,要判断一个向上转型是否成立,可以调用 isAssignableFrom()

1
2
3
4
5
6
7
8
// Integer i = ?
Integer.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Integer
// Number n = ?
Number.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Number
// Object o = ?
Object.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Object
// Integer i = ?
Integer.class.isAssignableFrom(Number.class); // false,因为Number不能赋值给Integer

后记

本文使用的实体类 Student

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
package top.uuanqin;

public class Student {
private String name;

private Integer id;

public Student() {
this("unset", 0);
}

public Student(String name, Integer id) {
super();
this.name = name;
this.id = id;
}

public String getName() {
return name;
}

@Override
public String toString() {
return "Student [name=" + name + ", id=" + id + "]";
}

public void setName(String name) {
this.name = name;
}

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public void sayHello() {
System.out.println("hello my name is " + this.name);
}

public void sayLocation(String location, int num) {
System.out.println("I am in " + location + " " + num);
}
}

知识点:Java 中的不定长参数

1
2
3
4
5
6
7
8
public static <T> List<T> asList(T... a) {
//不定长参数 a是一个数组
java.util.ArrayList<T> list = new ArrayList<T>();
for (int i = 0; i < a.length; i++) {
list.add(a[i]);
}
return list;
}

我的碎碎念

我感觉反射很奇妙,也很强大。有点像借尸还魂的感觉。

本文参考


  1. setAccessible(true) 可能会失败。如果 JVM 运行期存在 SecurityManager,那么它会根据规则进行检查,有可能阻止 setAccessible(true)。例如,某个 SecurityManager 可能不允许对 java 和 javax 开头的 package 的类调用 setAccessible(true),这样可以保证 JVM 核心库的安全。 ↩︎ ↩︎ ↩︎