Java 反射原理及方法
反射机制
类加载过程
类加载的完整过程如下:
- 在编译时,Java 编译器编译好 .java 文件之后,在磁盘中产生 .class 文件。.class 文件是二进制文件,内容是只有 JVM 能够识别的机器码。
- JVM 中的类加载器读取字节码文件,取出二进制数据,加载到内存中,解析.class 文件内的信息。类加载器会根据类的全限定名来获取此类的二进制字节流;然后,将字节流所代表的静态存储结构转化为方法区的运行时数据结构;接着,在内存中生成代表这个类的 java.lang.Class 对象。
- 加载结束后,JVM 开始进行连接阶段(包含验证、准备、初始化)。经过这一系列操作,类的变量会被初始化。
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 对象的三种方法:
- Class.forName 静态方法
- 类名.class
- Object 的 getClass 方法
1 | String s = "Hello"; |
因为 Class 实例在 JVM 中是唯一的,所以,每种方式获取的 Class 实例是同一个实例。可以用 == 比较两个Class实例。
类型判断与继承关系
判断是否为某个类的实例有两种方式:
- 用 instanceof 关键字
- 用 Class 对象的 isInstance 方法(它是一个 Native 方法)
instanceof 和 == 的区别:instanceof 不但匹配指定类型,还匹配指定类型的父类。而用 == ,则两边的类型必须严格相等,不能包含继承关系。
通过 Class 对象可以获取继承关系:
- Class getSuperclass():获取父类类型;
- Class[] getInterfaces():获取当前类实现的所有接口。
- 通过Class对象的isAssignableFrom()方法可以判断一个向上转型是否可以实现。
1 | // Integer 继承自 Number |
获取类名
当我们拿到 Class 对象后,就可以获取类的基本信息。 Class 提供了以下方法获取类名:
方法 | 用途 | 示例 |
---|---|---|
getName | 返回完整类名(JNI字段描述符) | [Lcom.test.TestClass$TestInnerClass |
getCanonicalName | 返回完整类名(Java描述符) | com.test.TestClass.TestInnerClass[] |
getSimpleName | 返回类名 | TestInnerClass[] |
类的数组,在 JVM 中是有单独的实例,与类的实例区别开。
1 | // 示例类 |
获取构造方法与创建实例
Class 对象提供以下方法获取对象的构造方法(Constructor):
方法 | 用途 |
---|---|
getConstructor | 返回类的特定 public 构造方法。参数为方法参数对应 Class 的对象。 |
getDeclaredConstructor | 返回类的特定构造方法。参数为方法参数对应 Class 的对象。 |
getConstructors | 返回类的所有 public 构造方法。 |
getDeclaredConstructors | 返回类的所有构造方法。 |
注意: Constructor 总是当前类定义的构造方法,和父类无关,因此不存在多态的问题。调用非 public 的 Constructor 时,必须首先通过 setAccessible(true) 设置允许访问。setAccessible(true)可能会失败。
通过反射来创建实例对象主要有三种方式:
- 用 Class 对象的 newInstance 方法。这种用法,只能调用该类的 public 无参数构造方法。
- 用 Constructor 对象的 newInstance 方法。这种用法,可以调用该类的任意构造方法。
- 用 Array.newInstance 创建数组的实例。
1 | // 使用 Constructor 构造实例: |
获取成员
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 | class Person { |
获取方法
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 | class Person { |
注意:使用反射调用方法时,仍然遵循多态原则:即总是调用实际类型的覆写方法(如果存在)。调用静态方法时,由于无需指定实例对象,所以invoke方法传入的第一个参数永远为null。
反射缺点
性能开销
- 变长参数方法导致的 Object 数组。由于 Method.invoke 是一个变长参数方法,在字节码层面它的最后一个参数会是 Object 数组。Java 编译器会在方法调用处生成一个长度为传入参数数量的 Object 数组,并将传入参数一一存储进该数组中。
- 基本类型的自动装箱、拆箱。由于 Object 数组不能存储基本类型,Java 编译器会对传入的基本类型参数进行自动装箱。
这两个操作除了带来性能开销外,还可能占用堆内存,使得 GC 更加频繁。
耗时问题
Class.forName 会调用 Native 方法,Class.getMethod 则会遍历该类的公有方法。如果没有匹配到,它还将遍历父类的公有方法。可想而知,这两个操作都非常费时。
注意,以 getMethod 为代表的查找方法操作,会返回查找得到结果的一份拷贝。因此,我们应当避免在热点代码中使用返回 Method 数组的 getMethods 或者 getDeclaredMethods 方法,以减少不必要的堆空间消耗。在实践中,我们往往会在应用程序中缓存 Class.forName 和 Class.getMethod 的结果。
破坏封装性
反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。
内部曝光
由于反射允许代码执行在非反射代码中非法的操作,例如访问私有字段和方法,所以反射的使用可能会导致意想不到的副作用,这可能会导致代码功能失常并可能破坏可移植性。反射代码打破了抽象,因此可能会随着平台的升级而改变行为。
JNI 字段描述符
JNI 字段描述符(Java Native Interface FieldDescriptors),它是一种对 Java 数据类型、数组、方法的编码。
这种编码方式把 Java 中的基本数据类型、数组、对象都使用一种规范来表示:
- 八种基本数据类型都使用一个大写字母表示。
- void 使用 V 表示。
- 数组使用左方括号表示。
- 方法使用一组圆括号表示,参数在括号里,返回类型在括号右侧。
- 对象使用 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