图 1:本文封面,反射是把类的「说明书」在运行时打开。
先说明属性:这是一篇经典题解析,题目出自本地历史面试资料,与任何公司的当前招聘无关。「简单介绍 Java 反射机制,反射在哪些地方有应用场景」是两连问:机制答不清是背书,场景答不出是没用过框架。这题的答题骨架是——一条工作链路、一串应用场景、三重代价。
第一步,工作链路:四步把「编译期决定」推迟到运行期。
普通代码在编译期就定死了调哪个类、哪个方法;反射把这一切推迟到运行时。链路四步:由全限定类名字符串拿到 Class 对象(Class.forName 或类字面量);从 Class 取出成员描述对象——Field、Method、Constructor 就是字段、方法、构造器的「运行时说明书」;调用前如需破开封装检查,用 setAccessible 关掉访问检查;最后 invoke 或 newInstance 动态执行。整条链路的关键不在 API,而在于:类的选择、方法的匹配、访问的检查,全部从编译期挪到了运行期。
第二步,应用场景:框架的通用装配能力全靠它。
官方教程列举的用途很准:可扩展性——按配置里的全限定名创建外部类的实例,这正是 IoC 容器装配 Bean 的底层动作,配置文件写类名、容器反射 new 出来再注入依赖;类浏览器与可视化开发环境——枚举一个类的成员;调试器与测试工具——检查私有成员、批量调用可发现的 API。再补两个工程里天天在用的:序列化框架按字段名读写私有字段,Agent 框架把工具方法注册成「字符串名字到方法」的动态分发表。没有反射,框架只能为每个业务类生成专用代码;有了反射,一套装配逻辑服务所有类。
第三步,用代码把链路和代价都跑出来。
下面这份 Java 程序完整走一遍四步链路,并实测直接调用与反射调用的耗时差距。用 JDK 17 编译运行(javac ReflectDemo.java && java ReflectDemo):
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
public class ReflectDemo {
// 待装配的目标类:框架眼里它只是一个「全限定类名 + 配置」
static class Greeter {
private String prefix = "你好";
public Greeter() {}
private String greet(String name) { return prefix + "," + name; }
public String shout(String name) { return prefix + "!" + name + "!"; }
}
public static void main(String[] args) throws Exception {
// 第一步:由类名字符串拿到 Class 对象(框架装配的起点)
Class<?> cls = Class.forName("ReflectDemo$Greeter");
System.out.println("Class 对象到手:" + cls.getName());
// 第二步:从 Class 取出构造器与方法描述对象
Constructor<?> ctor = cls.getDeclaredConstructor();
Method greet = cls.getDeclaredMethod("greet", String.class);
// 第三步:破开封装检查(greet 是 private,不这一步 invoke 会抛 IllegalAccessException)
Object obj = ctor.newInstance();
greet.setAccessible(true);
// 第四步:invoke 动态执行
System.out.println("反射调用 private 方法:" + greet.invoke(obj, "面试官"));
// 性能对比:直接调用 vs 反射调用,各一百万次
Greeter g = new Greeter();
Method shout = cls.getDeclaredMethod("shout", String.class);
int rounds = 1_000_000;
long t0 = System.nanoTime();
for (int i = 0; i < rounds; i++) g.shout("x");
long t1 = System.nanoTime();
for (int i = 0; i < rounds; i++) shout.invoke(g, "x");
long t2 = System.nanoTime();
System.out.printf("直接调用 100 万次:%d 毫秒%n", (t1 - t0) / 1_000_000);
System.out.printf("反射调用 100 万次:%d 毫秒(差距随 JIT 优化收窄,但反射始终多一层运行时检查)%n", (t2 - t1) / 1_000_000);
}
}
实测输出(JDK 17):
Class 对象到手:ReflectDemo$Greeter
反射调用 private 方法:你好,面试官
直接调用 100 万次:22 毫秒
反射调用 100 万次:26 毫秒(差距随 JIT 优化收窄,但反射始终多一层运行时检查)
注意两点:private 方法靠 setAccessible 成功调用了——反射能越过封装;性能差距在 JIT warmed up 之后没有传说中夸张,但反射调用始终多一层动态解析与访问检查,热路径上仍应避免。
图 2:反射的四步链路与三重代价。
可能的追问与易错点。
「反射的代价除了性能还有什么?」官方教程列得很全:安全限制——运行在安全管理器下可能没有反射权限,JDK 17 起模块系统强封装,对 JDK 内部类的深反射默认被拒绝;暴露内部——反射能做非反射代码不允许的事,破坏抽象后平台升级时行为可能悄悄变化;可维护性——编译器不再帮你检查类名拼写和参数类型,错误推迟到运行期才暴露。易错点三个:在每次调用前都 getMethod 查找,描述对象应该缓存复用;setAccessible 滥用绕过封装,给后续升级埋雷;把反射当常规业务工具,官方的建议是能不用就不用——它是框架的地基,不是业务代码的顺手工具。回到答题节奏:先给「编译期决定推迟到运行期」的定义,再走四步链路,然后列框架装配、序列化、调试测试三组场景,最后主动交代性能、安全、可维护性三重代价。你读过哪个框架的装配源码?欢迎分享印象最深的一段反射用法。