← 返回文章

反射

发布于 2026/10/19

1. 完整回答

Java 反射是指程序在运行时获取类的信息,并动态操作类或对象的一种机制。通过反射,可以获取一个类的构造方法、成员变量、方法和注解,还可以动态创建对象、调用方法以及读写字段。

反射主要解决的是程序在编译阶段无法确定具体类型或调用行为的问题,让代码具有更强的灵活性和扩展性。

它的核心原理是 JVM 在类加载后,会为类型提供对应的 Class 对象,程序可以通过这个对象获取类的结构信息。

具体使用时,首先获取目标类的 Class 对象,然后通过它获取 Constructor、Method 或 Field 等反射对象,最后通过这些对象完成实例化、方法调用或者字段访问。

比如,一个框架需要根据配置文件中的类名创建对象,就可以通过 Class.forName() 获取类信息,再通过构造方法动态创建实例。

反射最大的特点是动态性强、通用性好,能够降低框架代码和具体业务类之间的耦合。

不过,反射也存在一定的性能开销,而且会削弱编译期类型检查。访问非公开成员还会受到 Java 访问控制和模块系统的限制。

实际开发中,反射广泛应用于 Spring 依赖注入、MyBatis 对象映射、JSON 序列化、注解处理以及各种插件化框架。

2. 核心原理与关键概念

2.1 核心原理

Java 反射的基础是 JVM 的运行时类型信息。

类加载器将类加载到 JVM 后,JVM 会建立对应的运行时类型信息,并通过 Class 对象向 Java 程序提供访问入口。

反射的核心执行流程是:

步骤 A:获取 Class 对象

通过类字面量、已有对象或者完整类名获取 Class 对象。

步骤 B:获取成员信息

通过 Class 对象查找构造方法、成员变量、方法和注解,得到对应的反射对象。

步骤 C:检查访问权限

执行反射操作时,需要满足 Java 的访问控制规则。如果访问非公开成员,可以在允许的情况下通过 setAccessible(true) 关闭常规访问检查。

步骤 D:执行动态操作

通过反射对象完成实际操作:

  • Constructor.newInstance():创建对象

  • Method.invoke():调用方法

  • Field.get() / Field.set():读取或修改字段

整体流程:

获取 Class → 查找目标成员 → 检查访问权限 → 执行反射操作

在底层实现方面,Java 18 开始,JDK 核心反射改为基于方法句柄等机制实现。不过,对业务代码来说,主要使用方式仍然是 Class、Method、Field 和 Constructor 这些 API。

2.2 关键概念

核心概念 作用
Class 表示运行时类型信息,是反射的主要入口
Constructor 表示构造方法,可以动态创建对象
Method 表示方法,可以动态调用方法
Field 表示成员变量,可以读取或修改字段
AccessibleObject 为字段、方法和构造方法提供访问检查控制能力
Annotation 表示注解信息,可以配合反射实现基于注解的配置

2.3 获取 Class 对象的三种方式

// 1. 类字面量
Class<?> c1 = User.class;

// 2. 通过已有对象
Class<?> c2 = user.getClass();

// 3. 通过完整类名
Class<?> c3 = Class.forName("com.example.User");

三种方式最终都能够获取对应的运行时类型信息。

其中,Class.forName(String) 会在需要时触发类初始化,更适合运行时根据字符串动态确定类型。

3. 高频面试追问

3.1 Java 反射为什么能够在运行时操作类?

核心原因是 JVM 在运行时保留了类型信息,并通过 Class 对象提供访问入口。

普通方法调用通常在编译阶段就能确定目标方法的名称和参数类型,而反射可以在运行阶段再查找具体成员。

比如,框架拿到一个类名字符串后,可以先获取对应的 Class 对象,再查找指定的方法,最后通过 Method.invoke() 执行。

这样框架就可以在运行时处理不同的业务类,而不需要针对每个类单独编写调用逻辑。

不过,反射仍然受到 Java 类型规则和访问权限规则的约束。

3.2 Class.forName() 和 ClassLoader.loadClass() 有什么区别?

最主要的区别在于是否默认触发类初始化。

Class.forName(String) 默认会加载并初始化目标类。如果类还没有初始化,就会执行静态初始化逻辑。

ClassLoader.loadClass() 主要负责加载类,通常不会立即触发类初始化。

如果希望通过 Class.forName() 获取类型,但暂时不初始化,可以使用带 initialize 参数的重载方法,将它设置为 false。

两者都可以配合动态加载机制使用。框架如果需要控制类加载时机,或者使用自定义类加载器,就可以根据具体场景选择。

继续追问:反射和类加载有什么关系?

类加载为反射提供运行时类型信息,反射则利用这些信息查询和操作类的成员。

反射本身也不代表每次操作都会重新加载类。对于同一个类加载器定义的同一个类型,JVM 会复用对应的 Class 对象。

3.3 getMethod() 和 getDeclaredMethod() 有什么区别?

主要区别是查询范围和访问修饰符。

getMethod() 用于获取公共方法,查询范围包括当前类以及继承得到的公共方法。

getDeclaredMethod() 用于获取当前类自己声明的方法,包括 public、protected、包级访问和 private 方法,但不包含继承的方法。

例如,要获取父类提供的公共方法,可以使用 getMethod()。

如果要获取当前类声明的私有方法,就需要使用 getDeclaredMethod()。

另外,获取到私有方法的 Method 对象,并不代表已经获得调用权限。真正调用时仍然需要满足访问控制规则。

3.4 反射可以访问 private 成员吗?

可以,但需要满足访问权限和模块系统的限制。

正常情况下,Java 会对私有成员进行访问检查。

如果通过 getDeclaredMethod() 或 getDeclaredField() 获取到私有成员,可以在允许的情况下调用 setAccessible(true),使后续操作跳过常规 Java 访问检查。

不过,从 Java 9 引入模块系统开始,跨模块的深度反射受到限制。尤其是 Java 17 之后,JDK 内部 API 默认采用强封装。

如果目标模块没有向调用方开放对应包,即使调用 setAccessible(true),也可能抛出 InaccessibleObjectException。

所以实际开发中,我会优先使用公开 API,避免业务代码依赖私有成员的实现细节。

继续追问:setAccessible(true) 能修改所有 final 字段吗?

不能。它主要控制访问检查,并不保证所有字段都可以修改。

例如,static final 字段、Record 类的 final 字段,都不能通过这种方式获得写权限。

即使某些普通实例的 final 字段能够被反射修改,也可能因为编译器或 JVM 优化产生不可预期的行为。

3.5 反射为什么通常比直接调用慢?如何优化?

反射调用通常存在额外开销,但具体差距取决于 JDK 版本和调用场景。

直接调用的方法目标通常比较明确,JVM 更容易进行内联等优化。

反射需要处理动态目标、参数类型转换、访问权限检查等工作。如果每次调用前都重新查找方法,还会增加成员查找开销。

不过,Java 18 开始,核心反射已经基于方法句柄等机制重新实现,所以不能简单地认为反射一直依赖旧版本的本地方法调用机制。

实际优化时,我一般会采用三个办法:

  1. 缓存反射元数据,避免重复查找 Method、Field 和 Constructor。

  2. 尽量在初始化阶段完成反射扫描和对象创建,减少业务请求中的重复操作。

  3. 对性能敏感的高频动态调用,可以评估 MethodHandle,并通过性能测试确认收益。

反射非常适合框架初始化、配置解析和对象映射。对于简单、固定且调用频繁的方法,通常直接调用更合适。

3.6 Java 反射和动态代理有什么关系?

反射提供运行时类型操作能力,动态代理提供运行时的方法拦截和调用转发能力。

Java JDK 动态代理通常通过 Proxy 创建实现指定接口的代理对象。

当调用代理对象的方法时,请求会进入对应的 InvocationHandler.invoke(),开发者可以在这里添加日志、权限检查或者事务处理,再调用目标对象的方法。

在这个过程中,可以利用反射的 Method.invoke() 执行目标方法。

例如,调用一个订单服务方法时,代理对象可以先开启事务,再执行目标方法,最后提交或回滚事务。

需要注意的是,JDK 动态代理基于接口。对于没有接口的普通类,可以考虑使用基于子类生成的代理方案,例如 Spring 中常见的 CGLIB 代理。

继续追问:反射和 AOP 是什么关系?

AOP 主要解决横切逻辑的统一处理,比如事务、日志和权限控制。

在 Spring 中,AOP 通常通过动态代理实现,而反射可以参与方法信息获取和目标方法调用。

因此,反射可以作为 AOP 实现中的一种底层技术手段。

3.7 反射如何读取注解?为什么有些注解读取不到?

反射能够读取运行时保留的注解,关键取决于注解的保留策略。

Java 注解有三种常见的保留策略:

  • SOURCE:只保留在源代码阶段。

  • CLASS:保留在 class 文件中,但不要求运行时保留。

  • RUNTIME:运行时保留,可以通过反射读取。

如果希望框架在运行时处理自定义注解,就需要使用 @Retention(RetentionPolicy.RUNTIME)。

例如,框架可以通过反射获取方法上的注解,根据注解内容决定是否执行某种处理逻辑。

Spring 中不少注解驱动功能,就涉及对运行时注解元数据的读取和处理。

3.8 Spring 中哪些地方使用了反射?

Spring 在 Bean 创建、依赖注入、注解处理等场景中都会使用反射。

以依赖注入为例,Spring 在处理 Bean 时,可以读取类的构造方法、字段或者方法上的注解,确定需要注入的依赖。

之后,容器从 BeanFactory 中解析对应的依赖对象,再通过构造方法调用、字段赋值或者方法调用完成注入。

对于字段注入,Spring 可以借助反射完成成员访问和赋值。

整个过程可以概括为:

读取 Bean 元数据 → 识别注入点 → 解析依赖对象 → 完成依赖注入

除此之外,Spring 在处理部分生命周期方法和框架扩展机制时,也会使用反射。

反射带来的好处是容器能够统一管理各种业务类,而不需要为每个类编写专门的实例化和赋值逻辑。

3.9 项目中使用反射需要注意哪些问题?

我主要关注性能、访问控制、类型安全和异常处理。

首先,反射涉及动态查找和调用。如果在高并发业务中反复扫描类的全部成员,容易产生不必要的性能开销,因此一般会缓存已经解析好的反射对象。

其次,反射调用的错误往往在运行阶段才能暴露。比如方法名错误可能触发 NoSuchMethodException,参数类型不匹配可能触发 IllegalArgumentException。

如果目标方法内部抛出了异常,通过 Method.invoke() 调用时,通常会被包装为 InvocationTargetException,需要通过 getCause() 获取原始异常。

另外,私有成员访问还要考虑模块系统的限制,不能依赖 setAccessible(true) 一定成功。

如果项目需要实现插件加载、注解扫描或者通用对象映射,我会考虑使用反射;如果业务类和调用关系已经明确,我会优先采用普通方法调用,保持代码简单和类型安全。

4. 易混淆概念对比

4.1 直接调用与反射调用

对比维度 直接调用 反射调用
方法确定时机 通常在编译阶段确定方法引用 运行时查找目标方法
类型检查 编译期检查更充分 部分错误在运行时暴露
性能 通常更容易优化 存在动态调用开销
灵活性 适合固定调用关系 适合动态配置和通用框架
使用场景 常规业务逻辑 框架、插件、对象映射

4.2 getMethod() 与 getDeclaredMethod()

对比维度 getMethod() getDeclaredMethod()
public 方法 支持 支持
private 方法 不支持 支持查询
继承的方法 支持查询公共继承方法 不包含
查询范围 当前类及继承的公共方法 当前类声明的全部方法

两者都有获取多个方法的对应形式:getMethods() 和 getDeclaredMethods()。

4.3 Class.forName() 与 ClassLoader.loadClass()

对比维度 Class.forName(String) ClassLoader.loadClass(String)
主要作用 根据类名获取 Class 对象 通过指定类加载器加载类
默认初始化 会 通常不会
类加载器 使用调用方对应的类加载器 使用指定的类加载器
典型场景 运行时动态获取类型 自定义类加载、插件隔离

4.4 反射与 JDK 动态代理

对比维度 反射 JDK 动态代理
主要能力 动态获取和操作类成员 动态生成接口代理对象
核心 API Class、Method、Field Proxy、InvocationHandler
是否要求接口 不要求 要求代理接口
典型场景 对象创建、注解扫描 AOP、方法拦截

4.5 Method.invoke() 与 MethodHandle

对比维度 Method.invoke() MethodHandle
所属 API java.lang.reflect java.lang.invoke
调用方式 通过 Method 对象动态调用 通过有明确方法类型的句柄调用
访问检查 调用时按反射规则检查 主要在创建句柄时检查
性能特点 使用简单,存在反射调用开销 更利于 JVM 优化,实际性能取决于场景
适用场景 通用框架、动态调用 性能敏感的动态调用

5. 总结

Java 反射就是一种在运行时获取类型信息,并动态创建对象、调用方法和访问字段的机制。

它的核心依赖是 JVM 提供的 Class 对象,通过 Class 可以获取 Constructor、Method 和 Field 等反射对象,再执行具体操作。

反射最大的优势是灵活性强、扩展性好、能够降低框架和业务代码的耦合,所以 Spring 依赖注入、MyBatis 对象映射以及各种注解驱动框架都会使用反射。

相比普通方法调用,反射能够在运行时动态确定操作目标,但也会增加一定的调用开销,并且部分类型错误只能在运行时发现。