反射机制

类加载过程

类加载的完整过程如下:

  1. 在编译时,Java 编译器编译好 .java 文件之后,在磁盘中产生 .class 文件。.class 文件是二进制文件,内容是只有 JVM 能够识别的机器码。
  2. JVM 中的类加载器读取字节码文件,取出二进制数据,加载到内存中,解析.class 文件内的信息。类加载器会根据类的全限定名来获取此类的二进制字节流;然后,将字节流所代表的静态存储结构转化为方法区的运行时数据结构;接着,在内存中生成代表这个类的 java.lang.Class 对象。
  3. 加载结束后,JVM 开始进行连接阶段(包含验证、准备、初始化)。经过这一系列操作,类的变量会被初始化。

Java类加载过程

Class 对象

类是 JVM 在执行过程中动态加载的。JVM在第一次读取到一种类型时,将其加载进内存。每加载一种类,JVM 就为其创建一个 Class 类型的实例,并关联起来。

每个 Class 实例都是 JVM 内部创建的,其构造方法是 private,只有 JVM 能创建 Class 实例,我们自己的 Java 程序是无法创建 Class 实例的。一个 Class 实例包含了该类的所有信息,包括类名、包名、父类、实现的接口、所有方法、字段等,因此,如果获取了某个Class实例,我们就可以通过这个Class实例获取到该实例对应的class的所有信息。

反射调用

Java 中的 java.lang.reflect 包提供了反射功能。java.lang.reflect 包中的类都没有 public 构造方法。java.lang.reflect 包的核心接口和类如下:

类/接口 功能
Member 接口 反映关于单个成员(字段或方法)或构造函数的标识信息。
Field 类 提供一个类的域的信息以及访问类的域的接口。
Method 类 提供一个类的方法的信息以及访问类的方法的接口。
Constructor 类 提供一个类的构造函数的信息以及访问类的构造函数的接口。
Array 类 该类提供动态地生成和访问 JAVA 数组的方法。
Modifier 类 提供了 static 方法和常量,对类和成员访问修饰符进行解码。
Proxy 类 提供动态地生成代理类和类实例的静态方法。

获取 Class 对象

获取 Class 对象的三种方法:

  1. Class.forName 静态方法
  2. 类名.class
  3. Object 的 getClass 方法
1
2
3
4
5
6
7
8
9
10
11
String s = "Hello";

// 方法1:通过类的完整名获取
Class cls = Class.forName("java.lang.String");

// 方法2:通过类定义获取
Class cls = String.class;

// 方法3:通过类的实例获取
Class cls = s.getClass();

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

类型判断与继承关系

判断是否为某个类的实例有两种方式:

  1. 用 instanceof 关键字
  2. 用 Class 对象的 isInstance 方法(它是一个 Native 方法)

instanceof 和 == 的区别:instanceof 不但匹配指定类型,还匹配指定类型的父类。而用 == ,则两边的类型必须严格相等,不能包含继承关系。

通过 Class 对象可以获取继承关系:

  1. Class getSuperclass():获取父类类型;
  2. Class[] getInterfaces():获取当前类实现的所有接口。
  3. 通过Class对象的isAssignableFrom()方法可以判断一个向上转型是否可以实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Integer 继承自 Number
Integer n = new Integer(123);

boolean b1 = n instanceof Integer; // true
boolean b2 = n instanceof Number; // true

boolean b3 = Integer.class.isInstance(n) // true (b1)
boolean b4 = Number.class.isInstance(n) // true (b2)

boolean b5 = n.getClass() == Integer.class; // true
boolean b6 = n.getClass() == Number.class; // false

Number.class.isAssignableFrom(Integer.class); // true
Object.class.isAssignableFrom(Integer.class); // true
Integer.class.isAssignableFrom(Number.class); // false

获取类名

当我们拿到 Class 对象后,就可以获取类的基本信息。 Class 提供了以下方法获取类名:

方法 用途 示例
getName 返回完整类名(JNI字段描述符) [Lcom.test.TestClass$TestInnerClass
getCanonicalName 返回完整类名(Java描述符) com.test.TestClass.TestInnerClass[]
getSimpleName 返回类名 TestInnerClass[]

类的数组,在 JVM 中是有单独的实例,与类的实例区别开。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 示例类
Public class TestClass {

public static class TestInnerClass {

}
}

// 类数组
TestInnerClass[] test = new TestInnerClass[] {
new TestInnerClass(),
new TestInnerClass()
}

获取构造方法与创建实例

Class 对象提供以下方法获取对象的构造方法(Constructor):

方法 用途
getConstructor 返回类的特定 public 构造方法。参数为方法参数对应 Class 的对象。
getDeclaredConstructor 返回类的特定构造方法。参数为方法参数对应 Class 的对象。
getConstructors 返回类的所有 public 构造方法。
getDeclaredConstructors 返回类的所有构造方法。

注意: Constructor 总是当前类定义的构造方法,和父类无关,因此不存在多态的问题。调用非 public 的 Constructor 时,必须首先通过 setAccessible(true) 设置允许访问。setAccessible(true)可能会失败。

通过反射来创建实例对象主要有三种方式:

  1. 用 Class 对象的 newInstance 方法。这种用法,只能调用该类的 public 无参数构造方法。
  2. 用 Constructor 对象的 newInstance 方法。这种用法,可以调用该类的任意构造方法。
  3. 用 Array.newInstance 创建数组的实例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 使用 Constructor 构造实例:
Constructor constructor = Integer.class.getConstructor(int.class);
Integer n1 = (Integer) constructor.newInstance(123);

// 使用 Class.newInstance 构造实例:
StringBuilder sb = (StringBuilder) StringBuilder.class.newInstance();
sb.append("aaa");

// 用 Array.newInstance 创建数组的实例。
Class<?> cls = Class.forName("java.lang.String");
Object array = Array.newInstance(cls, 3);
Array.set(array, 0, "Scala");
Array.set(array, 1, "Java");
Array.set(array, 2, "Groovy");
System.out.println(Array.get(array, 1));

获取成员

Class 对象提供以下方法获取对象的成员(Field):

方法 用途
getFiled 根据名称获取公有的(public)类成员。
getDeclaredField 根据名称获取已声明的类成员。但不能得到其父类的类成员。
getFields 获取所有公有的(public)类成员。
getDeclaredFields 获取所有已声明的类成员。

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

  • getName():返回字段名称,例如,”name”;
  • getType():返回字段类型,也是一个Class实例,例如,String.class;
  • getModifiers():返回字段的修饰符,它是一个int,不同的bit表示不同的含义。

获取一个 Field 对象后,可以通过 Field.get(Object) 获取指定实例的指定成员的值,通过 Field.set(Object, Object) 设置指定成员的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person {
private String name;

public Person(String name) {
this.name = name;
}
}

Object person = new Person("Xiao Ming");

// 获取成员
Field field = person.getClass().getDeclaredField("name");

// 对于私有成员,先获取访问权限(可能会失败)
field.setAccessible(true);

// 获取实例的成员值
Object value = field.get(person);

// 设置实例的成员值
field.set(person, "Xiao Hong");

获取方法

Class 对象提供以下方法获取对象的方法(Method):

方法 用途
getMethod 返回类或接口的特定方法。其中第一个参数为方法名称,后面的参数为方法参数对应 Class 的对象。
getDeclaredMethod 返回类或接口的特定声明方法。其中第一个参数为方法名称,后面的参数为方法参数对应 Class 的对象。
getMethods 返回类或接口的所有 public 方法,包括其父类的 public 方法。
getDeclaredMethods 返回类或接口声明的所有方法,包括 public、protected、默认(包)访问和 private 方法,但不包括继承的方法。

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

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

获取一个 Method 对象后,可以用 invoke 方法来调用这个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person {
private String name;
private void setName(String name) {
this.name = name;
}
}

Object person = new Person();

// 获取方法
Method method = person.getClass().getDeclaredMethod("setName", String.class);

// 对于私有方法,先获取访问权限(可能会失败)
method.setAccessible(true);

// 通过 invoke 调用方法
method.invoke(person, "Bob");

注意:使用反射调用方法时,仍然遵循多态原则:即总是调用实际类型的覆写方法(如果存在)。调用静态方法时,由于无需指定实例对象,所以invoke方法传入的第一个参数永远为null。

反射缺点

性能开销

  1. 变长参数方法导致的 Object 数组。由于 Method.invoke 是一个变长参数方法,在字节码层面它的最后一个参数会是 Object 数组。Java 编译器会在方法调用处生成一个长度为传入参数数量的 Object 数组,并将传入参数一一存储进该数组中。
  2. 基本类型的自动装箱、拆箱。由于 Object 数组不能存储基本类型,Java 编译器会对传入的基本类型参数进行自动装箱。

这两个操作除了带来性能开销外,还可能占用堆内存,使得 GC 更加频繁。

耗时问题

Class.forName 会调用 Native 方法,Class.getMethod 则会遍历该类的公有方法。如果没有匹配到,它还将遍历父类的公有方法。可想而知,这两个操作都非常费时。

注意,以 getMethod 为代表的查找方法操作,会返回查找得到结果的一份拷贝。因此,我们应当避免在热点代码中使用返回 Method 数组的 getMethods 或者 getDeclaredMethods 方法,以减少不必要的堆空间消耗。在实践中,我们往往会在应用程序中缓存 Class.forName 和 Class.getMethod 的结果。

破坏封装性

反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。

内部曝光

由于反射允许代码执行在非反射代码中非法的操作,例如访问私有字段和方法,所以反射的使用可能会导致意想不到的副作用,这可能会导致代码功能失常并可能破坏可移植性。反射代码打破了抽象,因此可能会随着平台的升级而改变行为。

JNI 字段描述符

JNI 字段描述符(Java Native Interface FieldDescriptors),它是一种对 Java 数据类型、数组、方法的编码。

这种编码方式把 Java 中的基本数据类型、数组、对象都使用一种规范来表示:

  1. 八种基本数据类型都使用一个大写字母表示。
  2. void 使用 V 表示。
  3. 数组使用左方括号表示。
  4. 方法使用一组圆括号表示,参数在括号里,返回类型在括号右侧。
  5. 对象使用 L 开头,分号结束,中间是类的完整路径,包名使用正斜杠分隔。
Java 类型 JNI 字段描述符
boolean Z
byte B
char C
short S
int I
long J
float F
double D
void V
Object 以 L 开头,以;结尾,中间是使用 / 隔开的完整包名、类型,如果是内部类,添加 $ 符号分隔。(例如:Landroid/os/FileUtils$FileStatus;)
数组 使用 [ 表示,N维数组,用 N个[ 表示。
方法 使用 () 表示,参数在圆括号里,返回类型在圆括号右侧,(例如:(II) Z,表示 boolean func(int i,int j))

参考文档

https://dunwu.github.io/javacore/basics/java-reflection.html#_1-%E5%8F%8D%E5%B0%84%E7%AE%80%E4%BB%8B
https://www.liaoxuefeng.com/wiki/1252599548343744/1264799402020448
https://www.cnblogs.com/chanshuyi/p/head_first_of_reflection.html