





反射(Reflection),Java 的反射是指程序在运行期可以拿到一个对象的所有信息。
正常情况下,如果我们要调用一个对象的方法,或者访问一个对象的字段,通常会传入对象实例:
1 | // Main.java |
但是,如果不能获得 Person 类,只有一个 Object 实例,比如这样:
1 | String getFullName(Object obj) { |
怎么办?强制转型?
1 | String getFullName(Object obj) { |
强制转型的时候,你会发现一个问题:编译上面的代码,仍然需要引用 Person 类。不然,去掉import语句,不能编译通过
而反射是为了解决在运行期,对某个实例一无所知的情况下,如何调用其方法。
Class 类
除了 int 等基本类型外,Java 的其他类型全部都是 class(包括 interface )。
仔细思考,我们可以得出结论:class(包括 interface )的本质是数据类型(Type)。无继承关系的数据类型无法赋值:
1 | Number n = new Double(123.456); // OK |
而 class 是由 JVM 在执行过程中动态加载的。JVM 在第一次读取到一种 class 类型时,将其加载进内存。
每加载一种 class,JVM 就为其创建一个 Class 类型的实例,并关联起来。注意:这里的 Class 类型是一个名叫 Class 的 class。它长这样:
1 | public final class Class { |
以 String 类为例,当 JVM 加载 String 类时,它首先读取 String.class 文件到内存,然后,为 String 类创建一个 Class 实例并关联起来:
1 | Class cls = new Class(String); |
这个 Class 实例是 JVM 内部创建的,如果我们查看 JDK 源码,可以发现 Class 类的构造方法是 private,只有 JVM 能创建 Class 实例,我们自己的 Java 程序是无法创建 Class 实例的。
所以, JVM 持有的每个 Class 实例都指向一个数据类型(class或interface):
1 | ┌───────────────────────────┐ |
一个 Class 实例包含了该 class 的所有完整信息:
1 | ┌───────────────────────────┐ |
由于 JVM 为每个加载的 class 创建了对应的 Class 实例,并在实例中保存了该 class 的所有信息,包括类名、包名、父类、实现的接口、所有方法、字段等,因此,如果获取了某个 Class 实例,我们就可以通过这个 Class 实例获取到该实例对应的 class 的所有信息。
这种通过 Class 实例获取 class 信息的方法称为反射(Reflection)。
如何获取一个 class 的 Class 实例?有三个方法:
方法一:直接通过一个 class 的静态变量 class 获取:
1 | Class cls = String.class; |
方法二:如果我们有一个实例变量,可以通过该实例变量提供的 getClass() 方法获取:
1 | String s = "Hello"; |
方法三:如果知道一个 class 的完整类名,可以通过静态方法 Class.forName() 获取:
1 | Class cls = Class.forName("java.lang.String"); |
因为 Class 实例在 JVM 中是唯一的,所以,上述方法获取的 Class 实例是同一个实例。可以用 == 比较两个 Class 实例:
1 | Class cls1 = String.class; |
注意一下 Class 实例比较和 instanceof 的差别:
1 | Integer n = new Integer(123); |
用 instanceof 不但匹配指定类型,还匹配指定类型的子类。而用 == 判断 class 实例可以精确地判断数据类型,但不能作子类型比较。
通常情况下,我们应该用 instanceof 判断数据类型,因为面向抽象编程的时候,我们不关心具体的子类型。只有在需要精确判断一个类型是不是某个 class 的时候,我们才使用 == 判断 class 实例。
因为反射的目的是为了获得某个实例的信息。因此,当我们拿到某个 Object 实例时,我们可以通过反射获取该 Object 的 class 信息:
1 | void printObjectInfo(Object obj) { |
要从 Class 实例获取获取的基本信息,参考下面的代码:
1 | public class Main { |
注意到数组(例如 String[])也是一种 Class,而且不同于 String.class,它的类名是 [Ljava.lang.String。此外,JVM 为每一种基本类型如 int 也创建了 Class,通过 int.class 访问。
如果获取到了一个 Class 实例,我们就可以通过该 Class 实例来创建对应类型的实例:
1 | // 获取String的Class实例: |
上述代码相当于 new String()。通过 Class.newInstance() 可以创建类实例,它的局限是:只能调用 public 的无参数构造方法。带参数的构造方法,或者非 public 的构造方法都无法通过 Class.newInstance() 被调用。
动态加载
JVM 在执行 Java 程序的时候,并不是一次性把所有用到的 class 全部加载到内存,而是第一次需要用到 class 时才加载。例如:
1 | // Main.java |
当执行 Main.java 时,由于用到了 Main,因此,JVM 首先会把 Main.class 加载到内存。然而,并不会加载 Person.class,除非程序执行到 create() 方法,JVM 发现需要加载 Person 类时,才会首次加载 Person.class。如果没有执行 create() 方法,那么 Person.class根本就不会被加载。
这就是 JVM 动态加载 class 的特性。
动态加载 class 的特性对于 Java 程序非常重要。利用 JVM 动态加载 class 的特性,我们才能在运行期根据条件加载不同的实现类。例如,Commons Logging 总是优先使用 Log4j,只有当 Log4j 不存在时,才使用 JDK 的 logging。利用 JVM 动态加载特性,大致的实现代码如下:
1 | // Commons Logging优先使用Log4j: |
就是为什么我们只需要把 Log4j 的 jar 包放到 classpath 中,Commons Logging 就会自动使用 Log4j 的原因。
访问字段
对任意的一个 Object 实例,只要我们获取了它的 Class,就可以获取它的一切信息。
我们先看看如何通过 Class 实例获取字段信息。Class 类提供了以下几个方法来获取字段:
Field getField(name):根据字段名获取某个 public 的 field(包括父类)Field getDeclaredField(name):根据字段名获取当前类的某个 field(不包括父类)Field[] getFields():获取所有 public 的 field(包括父类)Field[] getDeclaredFields():获取当前类的所有 field(不包括父类)
我们来看一下示例代码:
1 | public class Main { |
上述代码首先获取 Student 的 Class 实例,然后,分别获取 public 字段、继承的 public 字段以及 private 字段,打印出的 Field :
1 | public int Student.score |
一个 Field 对象包含了一个字段的所有信息:
getName():返回字段名称,例如,name;getType():返回字段类型,也是一个Class实例,例如,String.class;getModifiers():返回字段的修饰符,它是一个 int,不同的 bit 表示不同的含义。
以 String 类的 value 字段为例,它的定义是:
1 | public final class String { |
我们用反射获取该字段的信息,代码如下:
1 | Field f = String.class.getDeclaredField("value"); |
利用反射拿到字段的一个 Field 实例只是第一步,我们还可以拿到一个实例对应的该字段的值。
例如,对于一个 Person 实例,我们可以先拿到 name 字段对应的 Field,再获取这个实例的 name 字段的值:
1 | public class Main { |
上述代码先获取 Class 实例,再获取 Field 实例,然后,用 Field.get(Object) 获取指定实例的指定字段的值。
运行代码,如果不出意外,会得到一个 IllegalAccessException,这是因为 name 被定义为一个 private 字段,正常情况下,Main 类无法访问 Person 类的 private 字段。要修复错误,可以将 private 改为 public,或者,在调用Object value = f.get(p); 前,先写一句:
1 | f.setAccessible(true); |
调用 Field.setAccessible(true) 的意思是,别管这个字段是不是 public,一律允许访问。再运行代码,就可以打印出 private 字段的值。
如果使用反射可以获取 private 字段的值,那么类的封装还有什么意义?
答案是正常情况下,我们总是通过 p.name 来访问 Person 的 name 字段,编译器会根据 public、protected 和 private 决定是否允许访问字段,这样就达到了数据封装的目的。
而反射是一种非常规的用法,使用反射,首先代码非常繁琐,其次,它更多地是给工具或者底层框架来使用,目的是在不知道目标实例任何信息的情况下,获取特定字段的值。
此外,setAccessible(true) 可能会失败。如果 JVM 运行期存在 SecurityManager,那么它会根据规则进行检查,有可能阻止 setAccessible(true) 。例如,某个 SecurityManager 可能不允许对 java 和 javax 开头的 package 的类调用 setAccessible(true),这样可以保证 JVM 核心库的安全。
设置字段值
通过 Field 实例既然可以获取到指定实例的字段值,自然也可以设置字段的值。
设置字段值是通过 Field.set(Object, Object) 实现的,其中第一个 Object 参数是指定的实例,第二个 Object 参数是待修改的值。示例代码如下:
1 | public class Main { |
运行上述代码,打印的 name 字段从 Xiao Ming 变成了 Xiao Hong,说明通过反射可以直接修改字段的值。
同样的,修改非 public 字段,需要首先调用 setAccessible(true)。
调用方法
我们已经能通过 Class 实例获取所有 Field 对象,同样的,可以通过 Class 实例获取所有 Method 信息。Class 类提供了以下几个方法来获取 Method:
Method getMethod(name, Class...):获取某个 public 的 Method(包括父类)Method getDeclaredMethod(name, Class...):获取当前类的某个 Method(不包括父类)Method[] getMethods():获取所有 public 的 Method(包括父类)Method[] getDeclaredMethods():获取当前类的所有 Method(不包括父类)
我们来看一下示例代码:
1 | public class Main { |
上述代码首先获取 Student 的 Class 实例,然后,分别获取 public 方法、继承的 public 方法以及 private 方法,打印出的 Method 类似:
1 | public int Student.getScore(java.lang.String) |
一个 Method 对象包含一个方法的所有信息:
getName():返回方法名称,例如:“getScore”;getReturnType():返回方法返回值类型,也是一个 Class 实例,例如:String.class;getParameterTypes():返回方法的参数类型,是一个 Class 数组,例如:{String.class, int.class};getModifiers():返回方法的修饰符,它是一个 int,不同的 bit 表示不同的含义。
当我们获取到一个 Method 对象时,就可以对它进行调用。我们以下面的代码为例:
1 | String s = "Hello world"; |
如果用反射来调用 substring 方法,需要以下代码:
1 | public class Main { |
对 Method 实例调用 invoke 就相当于调用该方法,invoke 的第一个参数是对象实例,即在哪个实例上调用该方法,后面的可变参数要与方法参数一致,否则将报错。
调用静态方法
如果获取到的 Method 表示一个静态方法,调用静态方法时,由于无需指定实例对象,所以 invoke 方法传入的第一个参数永远为 null。我们以 Integer.parseInt(String) 为例:
1 | public class Main { |
调用非 public 方法
和 Field 类似,对于非 public 方法,我们虽然可以通过 Class.getDeclaredMethod() 获取该方法实例,但直接对其调用将得到一个 IllegalAccessException 。为了调用非 public 方法,我们通过 Method.setAccessible(true) 允许其调用:
1 | public class Main { |
多态
我们来考察这样一种情况:一个 Person 类定义了 hello() 方法,并且它的子类 Student 也覆写了 hello() 方法,那么,从 Person.class 获取的 Method,作用于 Student 实例时,调用的方法到底是哪个?
1 | public class Main { |
运行上述代码,发现打印出的是 Student:hello,因此,使用反射调用方法时,仍然遵循多态原则:即总是调用实际类型的覆写方法(如果存在)。上述的反射代码:
1 | Method m = Person.class.getMethod("hello"); |
实际上相当于:
1 | Person p = new Student(); |
调用构造方法
我们通常使用new操作符创建新的实例:
1 | Person p = new Person(); |
如果通过反射来创建新的实例,可以调用 Class 提供的 newInstance() 方法:
1 | Person p = Person.class.newInstance(); |
调用 Class.newInstance() 的局限是,它只能调用该类的 public 无参数构造方法。如果构造方法带有参数,或者不是 public,就无法直接通过 Class.newInstance() 来调用。
为了调用任意的构造方法,Java 的反射 API 提供了 Constructor 对象,它包含一个构造方法的所有信息,可以创建一个实例。Constructor 对象和 Method 非常类似,不同之处仅在于它是一个构造方法,并且,调用结果总是返回实例:
1 | public class Main { |
通过 Class 实例获取 Constructor 的方法如下:
getConstructor(Class...):获取某个 public 的Constructor;getDeclaredConstructor(Class...):获取某个 Constructor`;getConstructors():获取所有 public 的Constructor;getDeclaredConstructors():获取所有Constructor。
注意 Constructor 总是当前类定义的构造方法,和父类无关,因此不存在多态的问题。
调用非 public 的 Constructor 时,必须首先通过 setAccessible(true) 设置允许访问。setAccessible(true) 可能会失败。
获取继承关系
当我们获取到某个 Class 对象时,实际上就获取到了一个类的类型:
1 | Class cls = String.class; // 获取到String的Class |
还可以用实例的 getClass() 方法获取:
1 | String s = ""; |
最后一种获取 Class 的方法是通过 Class.forName(""),传入 Class 的完整类名获取:
1 | Class s = Class.forName("java.lang.String"); |
这三种方式获取的 Class 实例都是同一个实例,因为 JVM 对每个加载的 Class 只创建一个 Class 实例来表示它的类型。
获取父类的 Class
有了 Class 实例,我们还可以获取它的父类的 Class:
1 | public class Main { |
运行上述代码,可以看到,Integer 的父类类型是 Number,Number 的父类是 Object,Object 的父类是 null。除 Object 外,其他任何非 interface 的 Class都必定存在一个父类类型。
获取 interface
由于一个类可能实现一个或多个接口,通过 Class 我们就可以查询到实现的接口类型。例如,查询 Integer 实现的接口:
1 | public class Main { |
运行上述代码可知,Integer 实现的接口有:
java.lang.Comparablejava.lang.constant.Constablejava.lang.constant.ConstantDesc
要特别注意:getInterfaces() 只返回当前类直接实现的接口类型,并不包括其父类实现的接口类型:
1 | public class Main { |
Integer 的父类是 Number,Number 实现的接口是 java.io.Serializable。
此外,对所有 interface 的 Class 调用 getSuperclass() 返回的是 null,获取接口的父接口要用 getInterfaces():
1 | System.out.println(java.io.DataInputStream.class.getSuperclass()); |
如果一个类没有实现任何 interface,那么 getInterfaces() 返回空数组。
继承关系
当我们判断一个实例是否是某个类型时,正常情况下,使用 instanceof 操作符:
1 | Object n = Integer.valueOf(123); |
如果是两个 Class 实例,要判断一个向上转型是否成立,可以调用 isAssignableFrom():
1 | // Integer i = ? |
动态代理
我们来比较 Java 的 class 和 interface 的区别:
- 可以实例化 class(非abstract);
- 不能实例化 interface。
所有 interface 类型的变量总是通过向上转型并指向某个实例的:
1 | CharSequence cs = new StringBuilder(); |
有没有可能不编写实现类,直接在运行期创建某个 interface 的实例呢?
这是可能的,因为 Java 标准库提供了一种动态代理(Dynamic Proxy)的机制:可以在运行期动态创建某个 interface 的实例。
什么叫运行期动态创建?听起来好像很复杂。所谓动态代理,是和静态相对应的。我们来看静态代码怎么写:
定义接口:
1 | public interface Hello { |
编写实现类:
1 | public class HelloWorld implements Hello { |
创建实例,转型为接口并调用:
1 | Hello hello = new HelloWorld(); |
这种方式就是我们通常编写代码的方式。
还有一种方式是动态代码,我们仍然先定义了接口 Hello,但是我们并不去编写实现类,而是直接通过 JDK 提供的一个 Proxy.newProxyInstance() 创建了一个 Hello 接口对象。这种没有实现类但是在运行期动态创建了一个接口对象的方式,我们称为动态代码。JDK 提供的动态创建接口对象的方式,就叫动态代理。
一个最简单的动态代理实现如下:
1 | public class Main { |
在运行期动态创建一个 interface 实例的方法如下:
- 定义一个
InvocationHandler实例,它负责实现接口的方法调用; - 通过
Proxy.newProxyInstance()创建 interface 实例,它需要3个参数:- 使用的
ClassLoader,通常就是接口类的ClassLoader; - 需要实现的接口数组,至少需要传入一个接口进去;
- 用来处理接口方法调用的
InvocationHandler实例。
- 使用的
- 将返回的 Object 强制转型为接口。
动态代理实际上是 JVM 在运行期动态创建 class 字节码并加载的过程,它并没有什么黑魔法,把上面的动态代理改写为静态实现类大概长这样:
1 | public class HelloDynamicProxy implements Hello { |
其实就是 JVM 帮我们自动编写了一个上述类(不需要源码,可以直接生成字节码),并不存在可以直接实例化接口的黑魔法。

