← 返回题目列表

Java 反射是什么?有什么优缺点?

高频 中等 第 10 / 32 题 更新于 2026/07/25
反射Class注解

简化版

反射(Reflection)让程序在运行时拿到一个类的完整信息——字段、方法、构造器、注解,并能动态地创建对象、调方法、读写字段。它是「框架的魔法」:Spring 的依赖注入、MyBatis 的 ORM、JSON 序列化,底层都靠它。代价是损失编译期检查、性能略低、可能破坏封装。

详细版

普通代码在编译期就定死了「调哪个类的哪个方法」;反射把这个决定推迟到运行时。入口是 Class 对象,三种拿法:

Class<?> c1 = User.class;                     // 编译期已知类型
Class<?> c2 = user.getClass();                // 从实例拿
Class<?> c3 = Class.forName("com.example.User"); // 只有字符串名,运行时加载

拿到 Class 后就能动态操作:

Object user = c3.getDeclaredConstructor().newInstance();  // 无参构造建对象
Method m = c3.getMethod("setName", String.class);         // 找方法
m.invoke(user, "Alice");                                  // 动态调用

框架为什么离不开它?因为框架在写的时候不知道你的业务类长什么样。Spring 扫到一个带 @Component 的类,只能靠反射去实例化、去找 @Autowired 的字段注入;MyBatis 拿到查询结果,只能靠反射把列名映射到你 POJO 的字段。缺点也很实在:编译器管不到反射调用,写错方法名要运行时才炸;反射调用比直接调用慢;setAccessible(true) 能撬开私有成员,破坏封装。

完整版教学

一、反射到底「反」的是什么

正常流程是「源码 → 类」:你写 new User(),编译期就把类型信息固化进字节码。反射是反过来:程序运行时,从一个已经加载的类「往回」读出它的结构(这些结构信息存在方法区 / 元空间里)。所以叫 reflection——程序在照镜子看自己。

它的能力边界是:只要类被 JVM 加载了,它的所有成员(包括私有的)都能被反射读到、操作到。

二、框架三大典型用法

  • 依赖注入(Spring):扫描注解找到 Bean,反射调构造器实例化,反射给 @Autowired 字段赋值。
  • ORM 映射(MyBatis / JPA):数据库列名 → 反射 set 到实体字段;实体字段 → 反射 get 拼成 SQL 参数。
  • 序列化(Jackson / Gson):反射遍历对象所有字段读值写成 JSON,或反过来把 JSON 值反射塞进字段。

共同点:框架代码在编译时不认识你的业务类,只能在运行时靠反射「临场」处理任意类型。这就是反射不可替代的价值。

三、代价一:性能

反射调用比直接调用慢,原因是它要做动态的方法查找、访问权限检查、参数装箱等额外工作,且难以被 JIT 内联优化。

但别夸大:现代 JVM 对反射有优化,且框架通常在启动时反射一次、之后缓存 Method/Field 对象复用。所以真正性能敏感的热点循环别频繁反射,一般业务完全无感。对极致场景,可用 MethodHandle 或 JDK 的动态字节码生成替代。

四、代价二:破坏编译期安全与封装

Method m = c.getMethod("setName", String.class);  // 方法名是字符串

方法名写成了字符串 "setName",编译器完全不检查——拼错了、参数类型变了,都要等运行时抛 NoSuchMethodException 才发现。这就把「编译期就能发现的错」拖到了线上。

setAccessible(true) 更要谨慎:它能强行访问 private 字段和方法,直接绕过封装。测试工具、序列化框架用它情有可原,业务代码里出现它,往往是设计有问题的信号。而且在 JPMS 模块化下,它可能被模块系统直接拒绝。

五、从扫描到调用的完整流程

成熟框架不会在每次业务请求中从头扫描反射。常见流程是启动期扫描类和注解,校验构造器或方法签名,把 ConstructorMethodField 或更高层访问器缓存成元数据;请求期只按缓存执行。这样既把配置错误尽早暴露,又把重复查找成本移出热点路径。

启动:扫描 classpath → 读取注解 → 校验签名 → 构建并缓存访问器
请求:查元数据缓存 → 转换参数 → invoke / MethodHandle → 处理异常

假设处理 10 万次对象映射:若每次都 getDeclaredFields() 并解析注解,就做 10 万轮结构发现;若按 Class<?> 缓存一次,结构发现只做 1 次,后续只剩字段读写。优化重点通常不是“完全禁止反射”,而是把发现与执行拆开并缓存不可变元数据。

方案编译期安全运行灵活性热点性能常见用途
直接调用最好已知类型的业务代码
传统反射有额外开销通用框架、工具
MethodHandle预热后更易优化动态调用基础设施
生成字节码/代码较高接近直接调用高性能序列化、代理

六、常见误区与追问

  • 误区:反射一定慢到不能用。 真正的问题是热点路径重复发现和检查,框架通过启动期解析与缓存降低成本。
  • 误区:getMethods()getDeclaredMethods() 等价。 前者包含可见的继承 public 方法,后者只取当前类声明的方法且包含非 public 成员。
  • 误区:setAccessible(true) 在任何环境都能突破 private。 JPMS 强封装和安全策略可能拒绝深反射,JDK 内部包尤其如此。
  • 追问:Class.forName() 会做什么? 默认会加载并初始化指定类,可能触发静态初始化;需要精细控制时可使用带 initialize 参数的重载。
  • 追问:Method.invoke() 抛出的业务异常如何取得? 目标方法异常会被包装为 InvocationTargetException,原异常在 getCause() 中。
  • 追问:泛型信息能否通过反射拿到? 签名元数据中的参数化类型通常可以读取,但类型擦除后运行时对象本身不能恢复所有具体类型实参。

记忆钩子:反射的正确姿势是“启动时发现并校验,请求时查缓存执行”,不是每次临场翻遍整个类。

反射调用还改变了异常表面。目标方法本来抛出 IllegalArgumentException,通过 Method.invoke 调用后,调用者首先收到的是 InvocationTargetException;框架若不解包 cause,日志和异常映射都会误判故障类型。

try {
    method.invoke(target, args);
} catch (InvocationTargetException e) {
    Throwable cause = e.getCause();
    if (cause instanceof RuntimeException runtime) {
        throw runtime;
    }
    throw new FrameworkInvocationException("调用失败", cause);
}

访问控制也应在启动期完成:发现方法不存在、参数数目不符或模块未开放时立即让应用启动失败,比等到第 10 万个请求才触发反射异常可靠得多。缓存键通常至少包含声明类、成员名和参数类型,因为 Java 允许同名重载,仅按名称缓存会调用错方法。

错误缓存键:User#set
可靠缓存键:User#set(java.lang.String)

如果目标类型可能被卸载,还要警惕静态缓存强引用 Class 导致类加载器泄漏;容器或插件系统常使用随生命周期清理的缓存,或谨慎选择弱引用结构。

七、加强记忆

反射以 Class 为入口,让程序在运行期发现构造器、字段、方法和注解,并动态创建或调用,因此框架才能处理编译时未知的业务类型。它以编译期安全、封装边界和额外调用成本换取通用性;工程上应缓存元数据、限制深反射、正确解包调用异常,并在稳定已知的业务路径优先直接 API。理解“发现阶段”和“执行阶段”的分离,比笼统背一句反射很慢更接近真实框架设计。