Java 反射
反射如何在运行时检查与调用成员:能力、成本、封装绕过与框架使用边界。
#concept
#growing
#dev
#java
[!info] related notes
Java 反射
这个概念为什么出现
框架要在不知道具体类的情况下注入依赖、映射 JSON、调用测试方法。反射提供运行时的类型信息与调用能力,但需要处理访问限制、调用失败和额外开销。
[!abstract] 一句话理解 反射通过
Class/Method/Field在运行时读取结构并调用;适合框架与工具,不适合业务热路径的默认写法。
最小可运行示例
场景:插件式计算器按方法名调用
import java.lang.reflect.Method;
public class ReflectCall {
public int add(int a, int b) { return a + b; }
public static void main(String[] args) throws Exception {
ReflectCall target = new ReflectCall();
Method m = ReflectCall.class.getMethod("add", int.class, int.class);
Object result = m.invoke(target, 2, 3);
System.out.println(result);
}
}
javac ReflectCall.java && java ReflectCall
期望:
5
结合场景再看三个关注点
- 方法查找按名与参数类型。
- 目标方法内部抛出的异常由
invoke包装为InvocationTargetException,原因为getCause();参数不匹配、无访问权限等反射调用错误不属于这种包装。 - 模块强封装可能阻止 setAccessible。
核心概念与准确模型
1. 入口
obj.getClass() / Class.forName / .class。单参数 Class.forName(name) 会初始化目标类;类字面量本身不触发初始化,Class.forName(name, false, loader) 可请求不初始化。
2. 成员访问
getMethod 查 public 方法(含继承);getDeclaredMethod 仅查本类声明(含非 public),查到不等于有权调用。查找要求参数类型匹配,不会把 Integer.class 自动当作 int.class;invoke 则支持规定的拆箱和基本类型拓宽转换。
3. 代价
- 动态查找、参数包装与访问检查可能带来额外成本;JIT 优化和缓存效果依赖 JDK 与调用方式,不能断言反射一律无法优化。
- 安全与封装风险
边界情况与反直觉行为
- 泛型擦除使反射拿不到完整运行时参数类型。
- 基本类型与包装类型在 Class 字面量上不同。
- Native Image 中无法静态推导的反射访问可能需要可达性元数据;不能照搬普通 JVM 的动态发现假设。
常见误区
[!warning] 常见误区:业务代码遍地反射 优先接口多态与生成代码。
工程实践
- 反射集中在框架边界,必要时缓存
Method对象以避免重复查找。 - 可评估
MethodHandle,但它与Method是不同 API,性能应实测。 exports允许对导出包的 public 类型和成员进行通常访问;跨模块访问非公开成员通常需要目标包opens。setAccessible(true)不能任意突破模块封装,可能抛InaccessibleObjectException;可用trySetAccessible()判断是否能开放访问。
本节总结
- 运行时元编程利器
- 成本与封装代价
- 框架常用、业务慎用
自测题
- getMethod 与 getDeclaredMethod 差别?
- 为什么 JSON 库爱反射或代码生成?
参考答案
- 前者看 public 成员(含继承);后者看本类声明成员含非 public。
- 需要在未知类型上读写属性;生成代码可降运行时成本。
延伸阅读与资料来源
| 资料 | 类型 | 支撑内容 |
|---|---|---|
| Reflection Trail | 教程 | 反射 |
| java.lang.reflect | API | 包 |