第一部分:反射的基本概念
1. 什么是反射?
反射是指在程序运行状态 中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性。这种动态获取信息以及动态调用对象方法的功能称为 Java 语言的反射机制。
2. 为什么需要反射?
在正常情况下,我们使用某个类时,都知道这个类是什么,有什么方法,然后直接通过 new 关键字创建对象并调用方法。这是一种"正向"操作。
而反射则相反,它允许我们在编译期不确定具体类型 的情况下,在运行期动态地加载类、创建对象、调用方法。这极大地提高了程序的灵活性和扩展性。
核心价值:反射将类的"动态加载"和"动态绑定"能力交给了程序员,使得框架(如 Spring)、工具(如 JUnit)和 IDE 能够实现。
第二部分:反射的核心 API
反射的核心 API 位于 java.lang.reflect 包中,主要涉及以下几个类:
-
Class类:代表一个类或接口。反射的根源。 -
Field类:代表类的成员变量。 -
Method类:代表类的方法。 -
Constructor类:代表类的构造方法。
获取 Class 对象的三种方式:
这是反射的起点。
-
Class.forName("全限定类名"):通过类的全路径名获取。最常用,体现了动态加载。 -
对象.getClass():通过对象实例获取。 -
类名.class:通过类字面常量获取。
java
// 1. Class.forName()
Class<?> clazz1 = Class.forName("java.lang.String");
// 2. .getClass()
String str = "Hello";
Class<?> clazz2 = str.getClass();
// 3. .class
Class<?> clazz3 = String.class;
System.out.println(clazz1 == clazz2); // true
System.out.println(clazz2 == clazz3); // true
第三部分:反射的底层原理
这是理解反射性能开销和实现机制的关键。
1. Class 对象 ------ 反射的基石
JVM 在加载一个类时(比如 java.lang.String),会在堆内存中为其创建一个唯一的 java.lang.Class 对象。这个 Class 对象就像是该类的"蓝图"或"镜像",它包含了该类的所有结构信息:
-
类名、包名、父类、实现的接口
-
字段信息(
Field[]) -
方法信息(
Method[]) -
构造器信息(
Constructor[]) -
访问修饰符等
无论你通过哪种方式获取 Class 对象,最终指向的都是 JVM 为这个类创建的同一个 Class 对象。
2. 方法的反射调用与 MethodAccessor
当我们通过 Method.invoke(obj, args...) 调用方法时,底层发生了什么?
在 JDK 1.8 及之前,invoke 的调用路径大致如下:
Method.invoke -> DelegatingMethodAccessorImpl.invoke -> NativeMethodAccessorImpl.invoke -> JNI(本地方法调用) -> 调用底层 C++ 代码。
-
初始状态 :第一次调用时,会使用一个本地实现(
NativeMethodAccessorImpl),它通过 JNI 调用到 JVM 内部的 native 代码来执行方法。这个调用路径很长,涉及 Java 到 C++ 的切换,所以性能开销很大。 -
动态生成字节码 :为了优化,Sun JDK 采用了一种技术。当一个方法被反射调用的次数超过一个阈值(默认15次)时,会动态生成一个
MethodAccessor的实现类,其invoke方法的字节码是直接用字节码生成技术(如sun.reflect.ClassFileGenerator)在内存中创建的。 -
这个生成的类,我们姑且称之为
GeneratedMethodAccessor1,它内部会直接调用目标方法,就像我们手写的obj.targetMethod(args)一样。这种方式避免了 JNI 的开销,因此后续的调用会快很多。这个过程被称为 Inflation(膨胀)。
为什么要有这个阈值?
因为生成字节码本身也是有成本的。对于只调用一两次的方法,生成字节码得不偿失,不如直接用本地方法。对于频繁调用的方法,生成字节码则能带来长期的性能收益。
在 JDK 1.9 及之后(模块化):
反射的实现被移到了 java.lang.invoke 包下,并且底层更多地依赖于 MethodHandle(方法句柄)。MethodHandle 在性能上通常优于传统的反射,并且提供了更精确的类型匹配。但基本原理类似,都是为了找到一种高效的方式来动态调用方法。
3. 反射的性能开销来源
-
方法调用开销:早期的 JNI 调用或动态字节码的生成和验证。
-
访问权限检查 :每次调用
Field.get/set或Method.invoke时,JVM 都需要检查访问权限(如private,protected)。即使我们调用了setAccessible(true)来取消检查,这个调用本身也有开销。 -
自动装箱/拆箱 :反射方法的参数是
Object[],所以基本类型需要频繁地进行装箱和拆箱。 -
方法签名解析:需要根据传入的参数类型来匹配最合适的方法。
优化建议:
-
在性能敏感的循环中,避免使用反射。
-
缓存
Class,Method,Field等对象。不要每次使用都重新查找。 -
对需要频繁访问的私有字段/方法,调用
setAccessible(true)来禁用安全检查。
第四部分:反射的应用场景
反射是许多 Java 框架和库的基石。
-
Spring 框架的 IoC 容器
-
Spring 在启动时,会扫描指定路径下的类,通过反射获取类的信息(如
@Component,@Autowired等注解)。 -
当需要创建 Bean 时,Spring 通过反射调用类的构造器(
Constructor.newInstance())来实例化对象。 -
对于依赖注入,Spring 通过反射(
Field.set()或Method.invoke())将依赖的 Bean 设置到目标字段或 setter 方法上。
-
-
JUnit 测试框架
-
JUnit 通过反射来查找测试类中所有带有
@Test注解的方法。 -
然后为每个测试方法创建一个新的测试类实例,并通过反射调用这些测试方法。
-
-
MyBatis 等 ORM 框架
- 将从数据库查询出的
ResultSet数据,通过反射(Field.set())映射到实体类(POJO)的对应字段上。
- 将从数据库查询出的
-
动态代理
- JDK 动态代理(
java.lang.reflect.Proxy)的核心就是利用反射,在运行时生成一个实现了指定接口的代理类,并将所有方法调用都转发到InvocationHandler.invoke()方法中。
- JDK 动态代理(
-
IDE 的智能提示
- IDE 通过反射来分析我们编写的代码,获取类的结构,从而提供代码补全、方法提示、类型检查等功能。
第五部分:代码示例
java
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class ReflectionDemo {
public static void main(String[] args) throws Exception {
// 1. 获取Class对象
Class<?> clazz = Class.forName("java.util.ArrayList");
// 2. 创建对象 (调用无参构造器)
Object list = clazz.getDeclaredConstructor().newInstance();
// 3. 获取方法并调用
Method addMethod = clazz.getMethod("add", Object.class);
addMethod.invoke(list, "Hello, Reflection!");
addMethod.invoke(list, 123);
// 4. 获取size方法并调用
Method sizeMethod = clazz.getMethod("size");
int size = (int) sizeMethod.invoke(list);
System.out.println("List size: " + size); // 输出: List size: 2
// 5. 访问内部数组字段 (注意:这是一个示例,实际ArrayList的elementData是private的)
// 通过setAccessible(true)绕过访问权限检查
Field elementDataField = clazz.getDeclaredField("elementData");
elementDataField.setAccessible(true); // 关键!取消访问检查
Object[] elementData = (Object[]) elementDataField.get(list);
System.out.println("Internal array length: " + elementData.length);
// 6. 打印List内容 (实际会调用toString,这里只是演示)
System.out.println("List content: " + list);
}
}
总结
| 方面 | 要点 |
|---|---|
| 核心思想 | 在运行时动态分析和操作类、对象、方法和属性。 |
| 底层原理 | 基于 JVM 的 Class 对象,通过本地方法或动态生成的字节码来实现动态调用。 |
| 性能开销 | 主要来自方法调用、权限检查、装箱拆箱。可通过缓存和 setAccessible 优化。 |
| 主要应用 | 框架设计(Spring IoC, MyBatis)、测试(JUnit)、动态代理、IDE 等。 |
| 优点 | 灵活性高、扩展性强,是构建复杂、可扩展架构的利器。 |
| 缺点 | 性能稍差、安全性问题(可绕过封装)、代码可读性降低。 |
理解反射的底层原理,能帮助你在"魔法般"的框架使用中,洞悉其本质,并能在合适的场景下正确地使用和优化它。