Java的反射机制是Java语言提供的一种强大特性,它允许程序在运行时检查和操作类、接口、字段和方法,而无需在编译时知道它们的具体名称。你可以把它理解为一种“自省”能力——程序可以审视自己,甚至修改自己的内部状态。
核心能力
通过反射,你可以在运行时动态地:
- 获取类的信息:获得某个类的构造函数、方法、字段、注解等信息。
- 创建实例:即使类名是动态的字符串,也能创建该类的对象。
- 调用方法:调用任意对象的方法,包括私有方法。
- 访问/修改字段:读取或修改对象的字段值,即使是私有字段。
- 操作数组:动态地创建和操作数组。
- 获取泛型信息:在运行时获取泛型类型的实际参数。
主要API
反射的核心类和接口都位于java.lang.reflect包中,主要有:
https://www.runoob.com/manual/jdk11api/java.base/java/lang/reflect/AccessibleObject.html
| 类/接口 | 作用 |
|---|---|
Class |
代表一个类或接口。它是反射的入口,几乎所有操作都从它开始。 |
Constructor |
代表类的构造函数,用于创建实例。 |
Method |
代表类的方法,用于执行方法。 |
Field |
代表类的字段(成员变量),用于获取或设置值。 |
Modifier |
用于解析访问修饰符(public、private等)。 |
Parameter |
代表方法的参数。 |
Class对象能够理解为是某个类的蓝图,通过这个蓝图就能够对这个类的对象进行修改和查阅信息的操作。
工作流程与示例
反射的常规流程是:先获取Class对象 → 再获取具体的Constructor、Method或Field → 最后进行操作。
1. 获取Class对象(三种方式)
// 方式1: 通过类名.class(编译时已知)
Class<Person> clazz1 = Person.class;
// 方式2: 通过对象.getClass()(运行时已知实例)
Person person = new Person();
Class<? extends Person> clazz2 = person.getClass();
// 方式3: 通过Class.forName()(最动态,类名为字符串)
Class<?> clazz3 = Class.forName("com.example.Person");
2. 创建实例
Class<?> clazz = Class.forName("java.util.ArrayList");
// 方式1: 使用无参构造(需要类有无参构造)
Object list1 = clazz.newInstance(); // Java 9后已过时
// 方式2: 获取Constructor对象再创建(推荐)
Constructor<?> constructor = clazz.getConstructor();
Object list2 = constructor.newInstance();
// 指定参数获取特定构造器
Constructor<?> paramConstructor = clazz.getConstructor(int.class);
Object list3 = paramConstructor.newInstance(10); // 初始容量10
3. 调用方法
Class<?> clazz = Class.forName("java.util.ArrayList");
Object list = clazz.getConstructor().newInstance();
// 获取add方法
Method addMethod = clazz.getMethod("add", Object.class);
// 调用list.add("Hello"),返回值是boolean
boolean result = (boolean) addMethod.invoke(list, "Hello");
// 获取size方法并调用
Method sizeMethod = clazz.getMethod("size");
int size = (int) sizeMethod.invoke(list);
System.out.println(size); // 输出1
4. 访问和修改私有字段
class Person {
private String name = "张三";
}
// 反射操作私有字段
Person p = new Person();
Field field = Person.class.getDeclaredField("name"); // getDeclaredField可获取私有字段
field.setAccessible(true); // 关键步骤:绕过访问控制检查
String oldName = (String) field.get(p);
System.out.println(oldName); // 输出"张三"
field.set(p, "李四");
System.out.println(field.get(p)); // 输出"李四"
主要用途
- 框架开发(最核心):Spring、Hibernate、MyBatis等框架大量使用反射。例如,Spring通过反射读取你添加了
@Component注解的类,然后动态创建和管理Bean。 - 通用工具和库:例如,
JUnit通过反射找到所有以@Test标记的方法并执行;JSON序列化库(如Jackson、Gson)通过反射读取对象的字段名和值。 - 动态代理:JDK的动态代理依赖反射,用于在运行时动态创建代理类,实现AOP(面向切面编程)。
- IDE和调试工具:IDE的代码补全、结构查看功能,都是利用反射来获取类信息。
- 绕过访问限制:用于测试、逆向工程或某些特殊场景(如操作内部API)。
性能与安全性考量
反射虽然强大,但绝非无代价的。
| 方面 | 问题 | 建议 |
|---|---|---|
| 性能 | 反射操作比直接调用慢10-100倍。因为涉及动态解析、类型检查、JIT优化困难。 | 避免在高频调用的核心代码中使用。如确需使用,可考虑缓存Method/Field对象(getMethod调用本身也耗时)。 |
| 安全性 | setAccessible(true)可以破坏封装,访问私有成员,可能违反原设计者的意图。 |
仅在必要时(如框架序列化)使用,普通业务代码应避免。 |
| 维护性 | 代码变得隐晦、不易读,IDE的重构和静态检查可能失效。 | 能用普通方式实现就不反射。反射应该作为“最后一根稻草”。 |
| 类型安全 | 编译时无法检测错误,如调用了不存在的方法会在运行时抛出NoSuchMethodException。 |
仔细处理异常,编写充分的单元测试。 |
总结
- 是什么:Java在运行时动态获取和操作类信息的能力。
- 为什么用:主要为了实现动态性和通用性,尤其在框架中,代码无法预知用户会传入什么类型。
- 怎么用:
Class→Constructor/Method/Field→newInstance()/invoke()/get()。 - 注意什么:性能较差,有安全风险,会破坏封装。
简而言之,反射是Java走向高度动态和灵活的重要基石,它让Java在一定程度上拥有了“脚本语言”的动态特性,但在普通业务代码中,应优先使用直接调用、接口和多态,只在编写框架、库或解决特定难题时,才请出反射这位“大能”。
