Java 反射:真的是“能用就行”的黑魔法吗?

举报
喵手 发表于 2025/12/08 20:20:53 2025/12/08
【摘要】 开篇语哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛  今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。  我是一名后端开发爱好者,工作日常接触到最多的就是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. 核心入口:ClassFieldMethodConstructor

反射 API 里几个关键角色:

  • Class<?>:类本身的运行时表示(每个类一个 Class 实例)
  • Field:字段的描述与操作入口
  • Method:方法的描述与调用入口
  • Constructor:构造方法的描述与调用入口

你可以这么理解:

  • Class 像是“类说明书”
  • FieldMethodConstructor 像是说明书里的各个章节索引

一个简单例子:

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 怎么配合这套东西?

简单脑补个流程:

  1. 类被加载(ClassLoader)→ 验证 → 准备 → 解析 → 初始化
  2. 类加载过程中,JVM 把 class 文件里的元数据信息解析出来,放到方法区(或者对应的元空间结构里)
  3. 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 创建真正的对象
  • 拿到 Methodinvoke

如果把类名字符串从配置文件读、从数据库读、从网络配置读——
那就变成了:“我可以在不改代码、不重新编译的情况下,动态指定要用哪个类”

这就是框架为什么离不开反射的根本原因之一。


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)动态查找 + 类型检查

反射调用一个方法,大致要经历:

  1. 找到 Method 对象(如果你每次都 getMethod 就更慢)
  2. 参数是 Object...,需要做类型检查、装箱/拆箱、强制转换
  3. 做访问性检查(必要时还要处理 setAccessible
  4. 再通过一个通用的调用入口去真正执行目标方法

直接调用的路径则是最短那条:

  • 编译期就确定了要调用哪个方法
  • JIT 可以内联、优化、消除各种边界

而反射路径是:“JVM 我们还是见一面再说吧”,
自然没法做太多激进优化。

(2)JIT 难以内联与优化

JIT 编译器很喜欢**“确定性”**:

  • 调用的是哪个类、哪个方法,它能推断
  • 对象生命周期它能分析
  • 然后干脆直接内联、常量折叠、循环优化

反射相当于在说:

“别问,问就是运行时我才告诉你我要调谁。”

那 JIT 也没办法提前规划,只能保守执行。

(3)频繁创建中间对象

比如 method.invoke(target, 1, 2)

  • 参数 12 要从 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 的开销

很多框架也都会对 ClassFieldMethod 做缓存。


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 → 依赖注入

核心套路其实都差不多:

  1. 用反射遍历类、方法、字段
  2. 读取注解信息
  3. 根据注解内容构建映射表、元数据
  4. 在运行时根据这些元数据做动作

举个迷你版例子:扫描某个包下的类,找出所有 @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 阶段
  • 运行时调用阶段

反射在这里完全就是“舞台后面的导演”。


七、总结:反射,该尊重它但别迷信它

最后我们把整篇内容压缩成几句更有“记忆点”的话:

  1. 反射是啥?

    • 利用 JVM 保存的类元数据,在运行时动态查看和操作类型信息的一整套机制
    • 核心类是 ClassFieldMethodConstructor
  2. 为什么慢?

    • 每次调用都要多做:查找、类型检查、装箱/拆箱、访问检查
    • JIT 难以内联和优化
    • 对比普通调用,通常是几倍到几十倍的差距(视场景而定)
  3. 在哪些地方必须用?

    • IOC / DI 框架(Spring)
    • ORM / 序列化 / 反序列化
    • 插件系统 / SPI / 动态加载
    • 注解驱动机制(控制器路由、实体映射等)
  4. 哪些地方别乱用?

    • 业务核心路径上频繁反射调用
    • 用字符串 + 反射代替接口、多态
    • 滥用 setAccessible(true) 改私有字段
  5. 怎么用更优雅?

    • 结果缓存(MethodField 等)
    • 把反射封装在框架/工具的边界层
    • 核心逻辑仍然是普通方法调用
    • 适当考虑 MethodHandle 等高级替代方案

… …

文末

好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。

… …

学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!

wished for you successed !!!


⭐️若喜欢我,就请关注我叭。

⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。


版权声明:本文由作者原创,转载请注明出处,谢谢支持!

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。