Java 反射:真的是“能用就行”的黑魔法吗?
开篇语
哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛
今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。
我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。
小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!
前言
说真的,每个写 Java 的人,多多少少都跟“反射”打过照面:要么是在 Spring 配置里看到一堆类名字符串奇怪地就变成对象了,要么是在调试报错时看见 java.lang.reflect 系列类一串栈信息。但你要真问一句:“反射到底是怎么工作的?为啥慢?该什么时候用、什么时候别用?”——很多人心里可能会咯噔一下:好像……一直当黑盒在用。
今天就来把这个黑盒拆了:
- 反射到底原理是啥?
- 为啥总说它有性能开销?
- 哪些地方不用反射会很爽,用了就是作死?
- 又有哪些场景不用反射根本玩不转?
整篇都从“工程实践者”视角聊,顺手给上代码例子,尽量讲人话,不端着。你可以一边看一边默默想:
“我是不是其实用反射多半是‘跟着框架走’,自己根本没搞懂?”😏
一、反射到底在干嘛?一句话说不清,但可以分三句说
先来个稍微正经一点的定义(但还是要讲人话):
反射(Reflection)就是:在运行时,动态地去“查看”和“操作”类的信息(类型、字段、方法、构造器),甚至还能“动态创建对象”的一种机制。
更白话一点就是:
-
平时我们写代码,是 “编译期就写死类和方法”:
UserService userService = new UserService(); userService.createUser(); -
而反射的思路是:“运行时才决定我要 new 谁、调谁的方法、给哪个字段赋值”:
Class<?> clazz = Class.forName("com.demo.UserService"); Object obj = clazz.getDeclaredConstructor().newInstance(); Method m = clazz.getMethod("createUser"); m.invoke(obj);
你可以把它想象成:
平时是“我跟编译器说:我要调谁”;
反射是“我自己在运行时翻字典:看看 strings 里写的是谁,然后再想办法把它搞出来”。
1. 反射依赖的前提:JVM 里有“元数据”
Java 的 class 不只是“字节码指令”,还包含了一堆类的描述信息(元数据),比如:
- 类名、包名
- 父类、接口
- 字段列表(名字、类型、修饰符)
- 方法列表(名字、参数类型、返回值、异常)
- 注解信息
反射干的第一件事,就是去把这些元数据读出来——而且是在运行时读。
2. 核心入口:Class、Field、Method、Constructor
反射 API 里几个关键角色:
Class<?>:类本身的运行时表示(每个类一个Class实例)Field:字段的描述与操作入口Method:方法的描述与调用入口Constructor:构造方法的描述与调用入口
你可以这么理解:
Class像是“类说明书”Field、Method、Constructor像是说明书里的各个章节索引
一个简单例子:
public class ReflectionBasicDemo {
public static void main(String[] args) throws Exception {
// 1. 拿到 Class 对象的几种方式
Class<String> clazz1 = String.class;
Class<?> clazz2 = Class.forName("java.lang.String");
String str = "hello";
Class<?> clazz3 = str.getClass();
System.out.println(clazz1 == clazz2); // true
System.out.println(clazz2 == clazz3); // true
// 2. 列出 String 的方法
Method[] methods = clazz1.getDeclaredMethods();
System.out.println("String has " + methods.length + " methods.");
for (int i = 0; i < 5; i++) { // 随便看前几个
System.out.println(methods[i].getName());
}
}
}
看着是不是有点“窥探内部”的意思?这就是反射的核心味道。
3. JVM 怎么配合这套东西?
简单脑补个流程:
- 类被加载(ClassLoader)→ 验证 → 准备 → 解析 → 初始化
- 类加载过程中,JVM 把 class 文件里的元数据信息解析出来,放到方法区(或者对应的元空间结构里)
Class.forName返回的Class实例,其实就是让你有机会去“操作这些元数据”
所以反射本质上不是魔法,而是:
利用 JVM 已经保存好的类信息,在运行时提供一个“通用 API”去操作类型。
二、先来点“感性认识”:反射怎么用?(看得见的黑魔法)
如果只是定义再定义,很快就困了,我们直接上几个实际例子。
1. 动态创建对象:Class.newInstance / Constructor.newInstance
假设有这么一个类:
public class User {
private String name;
private int age;
public User() {
System.out.println("User() constructor");
}
public User(String name, int age) {
this.name = name;
this.age = age;
}
public void hello() {
System.out.println("Hello, I'm " + name + ", age " + age);
}
}
我们用反射来创建它:
public class NewInstanceDemo {
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName("com.demo.User");
// 1. 调用无参构造
Object obj1 = clazz.getDeclaredConstructor().newInstance();
System.out.println("obj1 class = " + obj1.getClass());
// 2. 调用有参构造
Constructor<?> constructor = clazz.getConstructor(String.class, int.class);
Object obj2 = constructor.newInstance("Alice", 20);
Method hello = clazz.getMethod("hello");
hello.invoke(obj2);
}
}
运行结果大概是:
User() constructor
obj1 class = class com.demo.User
Hello, I'm Alice, age 20
这里你就能看到几个典型动作:
- 通过类名字符串得到
Class - 根据构造方法签名找到对应
Constructor newInstance创建真正的对象- 拿到
Method再invoke
如果把类名字符串从配置文件读、从数据库读、从网络配置读——
那就变成了:“我可以在不改代码、不重新编译的情况下,动态指定要用哪个类”。
这就是框架为什么离不开反射的根本原因之一。
2. 操作私有字段:setAccessible(true) 这把双刃剑 🔪
再来点刺激的:直接改私有字段。
public class PrivateFieldDemo {
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName("com.demo.User");
Object user = clazz.getDeclaredConstructor().newInstance();
Field nameField = clazz.getDeclaredField("name");
nameField.setAccessible(true); // 暴力破墙
nameField.set(user, "Bob");
Field ageField = clazz.getDeclaredField("age");
ageField.setAccessible(true);
ageField.setInt(user, 30);
Method hello = clazz.getMethod("hello");
hello.invoke(user);
}
}
输出:
User() constructor
Hello, I'm Bob, age 30
你会发现:
私有字段在反射面前,是纸糊的。
这当然很爽,但也很危险——
随便越过封装、规则、校验,长久下来,代码边界感会越来越差。
所以一般建议:
- 在测试、工具、框架内部可以适度使用
- 在业务代码里尽量少“暴力反射”,否则以后谁改字段名字谁倒霉
3. 动态调用方法:做一个“小型路由器”
一个常见场景是:你想根据字符串去调用不同的方法,例如一个简单的“命令执行器”:
public class CommandHandler {
public void create() {
System.out.println("create executed");
}
public void delete() {
System.out.println("delete executed");
}
public void update() {
System.out.println("update executed");
}
public void execute(String command) throws Exception {
Method method = this.getClass().getMethod(command);
method.invoke(this);
}
public static void main(String[] args) throws Exception {
CommandHandler handler = new CommandHandler();
handler.execute("create");
handler.execute("delete");
handler.execute("update");
}
}
这里 execute("create") 就像一个小型“控制器”,
通过方法名字符串,去找到对应的真实方法,这就是最原始的“路由”。
很多 Web 框架的请求分发,在底层都有类似的影子:
- URL / 注解 → 方法映射
- 通过反射找到方法、解析参数,再
invoke调用
三、反射为什么“慢”?到底慢在哪儿?🏃♂️💨
很多人听到的版本是:“反射性能很差,能不用就别用”。
但如果你问:“它慢是慢在哪?慢几倍?有没有办法缓解?”
答案往往就模糊了。
我们一点点拆开讲。
1. 反射调用 vs 直接调用:一个小测试
先写个对比代码(重点在结构,不必纠结微观数据):
public class PerformanceDemo {
public static class Target {
public int add(int a, int b) {
return a + b;
}
}
public static void main(String[] args) throws Exception {
Target target = new Target();
int iterations = 50_000_000;
// 1. 直接调用
long start1 = System.nanoTime();
int result1 = 0;
for (int i = 0; i < iterations; i++) {
result1 = target.add(1, 2);
}
long end1 = System.nanoTime();
System.out.println("Direct call: " + (end1 - start1) / 1_000_000 + " ms, result = " + result1);
// 2. 反射调用
Method method = Target.class.getMethod("add", int.class, int.class);
long start2 = System.nanoTime();
int result2 = 0;
for (int i = 0; i < iterations; i++) {
result2 = (int) method.invoke(target, 1, 2);
}
long end2 = System.nanoTime();
System.out.println("Reflect call: " + (end2 - start2) / 1_000_000 + " ms, result = " + result2);
}
}
通常你会看到:
- 反射调用比直接调用慢好几倍,甚至几十倍
- 具体多少倍跟 JVM 版本、JIT 优化、是否 warmed up 等因素有关
这里没必要追究精确数字,重点是 数量级差距。
2. 为什么反射会慢?几大主要原因
(1)动态查找 + 类型检查
反射调用一个方法,大致要经历:
- 找到
Method对象(如果你每次都getMethod就更慢) - 参数是
Object...,需要做类型检查、装箱/拆箱、强制转换 - 做访问性检查(必要时还要处理
setAccessible) - 再通过一个通用的调用入口去真正执行目标方法
直接调用的路径则是最短那条:
- 编译期就确定了要调用哪个方法
- JIT 可以内联、优化、消除各种边界
而反射路径是:“JVM 我们还是见一面再说吧”,
自然没法做太多激进优化。
(2)JIT 难以内联与优化
JIT 编译器很喜欢**“确定性”**:
- 调用的是哪个类、哪个方法,它能推断
- 对象生命周期它能分析
- 然后干脆直接内联、常量折叠、循环优化
反射相当于在说:
“别问,问就是运行时我才告诉你我要调谁。”
那 JIT 也没办法提前规划,只能保守执行。
(3)频繁创建中间对象
比如 method.invoke(target, 1, 2):
- 参数
1、2要从int装箱成Integer - 返回值
Object要再拆箱成int
这些装箱/拆箱、多态分派,全都是额外开销。
所以结论是:反射本质是 通用、灵活,但中间步骤多,
你为灵活性付出的是性能账单。
四、那反射还能不能用?要不用,那 Spring 怎么活?😅
别慌,虽然我们刚刚把反射批判了一通,但有些地方不用反射,世界是跑不动的。
关键不是“用不用”,而是:
“在哪用、用多少、怎么用得不那么作死。”
1. 使用反射特别合理、几乎必然会用到的场景
✅ 场景 1:IOC / DI 容器(Spring、Guice 之类)
依赖注入框架要做的事情大概是:
- 读取配置(XML / 注解 / Java Config)
- 根据类名、注解等信息创建对象
- 填充字段、调用 setter
- 执行初始化方法
这些全是“运行时才知道的东西”:
- 你配置里写的是哪个实现类
- 某个接口到底绑定的是哪个 Bean
- 使用哪个构造器、哪个字段需要注入
不用反射,几乎无解。
内部一定是类似这样的:
Class<?> clazz = Class.forName(className);
Object bean = clazz.getDeclaredConstructor().newInstance();
// 找有 @Autowired 的字段、方法,反射注入
✅ 场景 2:ORM(Hibernate、MyBatis-Plus 等)
ORM 要把:
- 数据库里的表字段 → Java 对象属性
- Java 对象属性 → SQL 参数
典型姿势:
- 读取类和字段上的注解
- 通过字段名字、getter/setter 方法来读写属性
- 对象转 Map、Map 转对象
这里也离不开反射,也经常配合注解使用:
Field field = clazz.getDeclaredField(columnNameMapping.get("user_name"));
field.setAccessible(true);
Object value = field.get(entity);
✅ 场景 3:序列化 / 反序列化(JSON、XML 等)
比如你用 Jackson、FastJSON、Gson:
- JSON 字符串 → Java 对象
- Java 对象 → JSON 字符串
框架内部要根据字段名、注解(@JsonProperty 等)去:
- 找字段
- 找 getter/setter
- 读值 / 写值
没有反射根本没法通用化。
✅ 场景 4:插件系统 / 脚本引擎 / SPI 拓展
你想做一个“插件式系统”,插件是单独的 jar,
主程序只知道插件按某个接口实现,要动态加载:
Class<?> pluginClass = classLoader.loadClass(pluginClassName);
Object plugin = pluginClass.getDeclaredConstructor().newInstance();
Plugin p = (Plugin) plugin;
p.run();
包括 Java 自带的 ServiceLoader 机制,本质也是通过反射来加载实现类,
这些都属于“反射用得很正常的地方”。
2. 哪些地方用反射容易把自己坑死?
❌ 坑 1:业务核心逻辑频繁反射调用
比如你写了个类似这样的工具:
public class BadInvoker {
public static Object invoke(Object target, String methodName, Object... args) throws Exception {
Class<?> clazz = target.getClass();
Class<?>[] argTypes = Arrays.stream(args)
.map(Object::getClass)
.toArray(Class<?>[]::new);
Method method = clazz.getMethod(methodName, argTypes);
return method.invoke(target, args);
}
}
然后你在业务逻辑里到处这么用:
BadInvoker.invoke(orderService, "createOrder", request);
BadInvoker.invoke(orderService, "payOrder", orderId);
看起来“很灵活”,但实际上:
- 性能开销大(每次都 getMethod,还推断参数类型)
- 没有编译期检查,方法名写错要等到运行时报错
- 重构、重命名方法时,IDE 没法帮你改字符串
这种地方用反射,属于让自己原本可以被编译器发现的错误全部延后到运行时,
本身就是对可靠性的一种损伤。
❌ 坑 2:滥用 setAccessible(true),到处“戳私有字段”
如果你在业务代码到处写:
field.setAccessible(true);
field.set(obj, value);
一时爽,维护人火葬场。
- 封装被破坏
- setter 里的校验、逻辑被绕过
- 字段名一改,所有反射调用全挂
这东西适合作为:
- 测试工具
- 框架内部的“底层实现”
但不适合作为业务层通用写法。
五、如何优雅地用反射:几条实用建议
反射不能不用,但可以用得更聪明点。
1. 缓存反射结果,避免重复开销
比如在高频调用场景,不要每次都 clazz.getMethod:
public class MethodCacheInvoker {
private static final Map<String, Method> METHOD_CACHE = new HashMap<>();
public static Object invoke(Object target, String methodName, Class<?>[] paramTypes, Object... args) throws Exception {
Class<?> clazz = target.getClass();
String key = clazz.getName() + "#" + methodName + Arrays.toString(paramTypes);
Method method = METHOD_CACHE.get(key);
if (method == null) {
method = clazz.getMethod(methodName, paramTypes);
method.setAccessible(true);
METHOD_CACHE.put(key, method);
}
return method.invoke(target, args);
}
}
这样至少:
getMethod的反射查找开销不会在每次调用时重复发生- 只剩下
invoke的开销
很多框架也都会对 Class、Field、Method 做缓存。
2. 只在边界层用反射,核心逻辑用“正常调用”
比如:
- IOC 容器用反射创建 Bean、注入依赖
- 但业务层一旦拿到 Bean,就用正常方法调用
- 不要在核心业务路径上继续搞“反射调用链”
你可以把反射想象成“底层机制”,
但对上层来说要尽量保持“看起来像普通方法调用”。
3. 能通过接口、多态解决的,就别用“字符串 + 反射”
例如,你想搞“根据不同类型执行不同逻辑”:
❌ 不推荐写成这样:
public void handle(String type) throws Exception {
Method method = this.getClass().getMethod("handle" + type);
method.invoke(this);
}
✅ 更推荐是老老实实搞一个策略模式 / 多态:
interface Handler {
void handle();
}
class AHandler implements Handler { public void handle() {...} }
class BHandler implements Handler { public void handle() {...} }
// Map<String, Handler> handlerMap
handlerMap.get(type).handle();
- 逻辑更清晰
- 编译期就能检查
- IDE 重构友好
反射是最后的兜底手段,不是“偷懒万能钥匙”。
4. JDK 8+ 的 MethodHandle / Lambda 也是替代选项
如果你确实需要“某种形式的动态调用”,
又觉得传统反射太慢,可以了解下:
java.lang.invoke.MethodHandle/MethodHandles.Lookup- 把方法引用转为 lambda / 函数式接口
这块属于更底层和偏进阶的内容,这里先按下不表,只留一句话:
在极端性能敏感场景,需要动态调用时,可以研究
MethodHandle替代传统反射。
六、反射 + 注解:框架的“元编程组合拳”
说到反射,不得不拉上它的好搭档:注解。
这俩组合起来,可以做到很多“看起来像魔法”的事,比如:
@Controller+@RequestMapping→ HTTP 请求路由@Entity+@Column→ ORM 映射@Autowired→ 依赖注入
核心套路其实都差不多:
- 用反射遍历类、方法、字段
- 读取注解信息
- 根据注解内容构建映射表、元数据
- 在运行时根据这些元数据做动作
举个迷你版例子:扫描某个包下的类,找出所有 @MyService 标记的类:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyService {
String value() default "";
}
@MyService("userService")
public class UserService {
public void hello() {
System.out.println("Hello from UserService");
}
}
反射扫描(简单示意版,不做复杂包扫描逻辑,只演示核心):
public class AnnotationScanner {
public static void main(String[] args) throws Exception {
// 假装我们已经拿到了某些 Class(真实环境一般是通过 ClassPath 扫描)
Class<?> clazz = UserService.class;
if (clazz.isAnnotationPresent(MyService.class)) {
MyService myService = clazz.getAnnotation(MyService.class);
String beanName = myService.value();
Object instance = clazz.getDeclaredConstructor().newInstance();
System.out.println("Found service bean: " + beanName + " -> " + instance);
}
}
}
这一套玩熟了,你就大概能理解各种框架的:
- 扫描阶段
- 注册 Bean 阶段
- 运行时调用阶段
反射在这里完全就是“舞台后面的导演”。
七、总结:反射,该尊重它但别迷信它
最后我们把整篇内容压缩成几句更有“记忆点”的话:
-
反射是啥?
- 利用 JVM 保存的类元数据,在运行时动态查看和操作类型信息的一整套机制
- 核心类是
Class、Field、Method、Constructor
-
为什么慢?
- 每次调用都要多做:查找、类型检查、装箱/拆箱、访问检查
- JIT 难以内联和优化
- 对比普通调用,通常是几倍到几十倍的差距(视场景而定)
-
在哪些地方必须用?
- IOC / DI 框架(Spring)
- ORM / 序列化 / 反序列化
- 插件系统 / SPI / 动态加载
- 注解驱动机制(控制器路由、实体映射等)
-
哪些地方别乱用?
- 业务核心路径上频繁反射调用
- 用字符串 + 反射代替接口、多态
- 滥用
setAccessible(true)改私有字段
-
怎么用更优雅?
- 结果缓存(
Method、Field等) - 把反射封装在框架/工具的边界层
- 核心逻辑仍然是普通方法调用
- 适当考虑
MethodHandle等高级替代方案
- 结果缓存(
… …
文末
好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。
… …
学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!
wished for you successed !!!
⭐️若喜欢我,就请关注我叭。
⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。
版权声明:本文由作者原创,转载请注明出处,谢谢支持!
- 点赞
- 收藏
- 关注作者
评论(0)