在上一篇中,我们学会了如何获取 Class 对象,并验证了它在 JVM 中的唯一性。本篇将在此基础上,利用反射完成更“激进”的操作:动态创建对象(绕过 new)、调用私有方法、修改私有字段,并对比反射调用与直接调用的性能差异。通过这些实验,你将真正理解框架(如 Spring、MyBatis)底层是如何“暴力”注入依赖的。

目录

一、引言:反射还能做什么?

二、实验准备:延续上一篇的 Student 类

三、实验二:通过反射创建对象、调用私有方法、修改私有字段

📸 截图1:Experiment2.java 代码截图

📸 截图2:运行结果

预期输出(你的实际输出应与之类似):

四、原理分析:setAccessible(true) 的威力

五、实验三:反射性能对比(直接调用 vs 反射调用)

📸 截图3:Experiment3.java 代码截图

📸 截图4:运行结果

典型输出(你的实际数值可能略有不同):

六、性能差异原因及优化建议

七、心得体会:反射是一把双刃剑

参考文献


一、引言:反射还能做什么?

反射的核心能力不仅仅是拿到 Class 对象,更重要的是 “运行时操作”。在实际开发中,很多框架需要在完全不知道类结构的情况下,动态地创建对象、执行方法、甚至修改内部状态。例如:

  • Spring IoC 容器根据配置文件中的类名字符串,创建 Bean 实例。

  • JUnit 根据 @Test 注解,动态调用测试方法。

  • MyBatis 将数据库查询结果自动映射到实体类的私有字段上。

这些场景都离不开反射的 ConstructorMethodField 以及 setAccessible(true)。本篇将通过三个实验,带你亲手实现这些操作,并思考反射的代价。


二、实验准备:延续上一篇的 Student 类

我们依然使用 Student 类,它包含私有字段、公有/私有方法,方便演示。

package com.reflection.demo;

public class Student {
    private String name;
    private int age;

    public Student() {
        this.name = "默认学生";
        this.age = 18;
    }

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void study() {
        System.out.println(name + " 正在学习,年龄:" + age);
    }

    private void secret() {
        System.out.println("这是一个私有方法,只能被反射调用");
    }

    @Override
    public String toString() {
        return "Student{name='" + name + "', age=" + age + "}";
    }
}

如果还没有这个类,请参考上一篇创建。本篇所有代码均基于该类。


三、实验二:通过反射创建对象、调用私有方法、修改私有字段

新建 Experiment2.java,代码如下(完整可运行):

import com.reflection.demo.Student;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Field;

public class Experiment2 {
    public static void main(String[] args) throws Exception {
        // 1. 获取 Class 对象
        Class<?> clazz = Class.forName("com.reflection.demo.Student");

        // 2. 通过无参构造器创建对象
        Object obj1 = clazz.getDeclaredConstructor().newInstance();
        System.out.println("无参构造创建的对象:" + obj1);

        // 3. 通过有参构造器创建对象
        Constructor<?> cons = clazz.getDeclaredConstructor(String.class, int.class);
        Object obj2 = cons.newInstance("张三", 22);
        System.out.println("有参构造创建的对象:" + obj2);

        // 4. 调用公有方法 study
        Method studyMethod = clazz.getDeclaredMethod("study");
        studyMethod.invoke(obj2);

        // 5. 调用私有方法 secret(暴力反射)
        Method secretMethod = clazz.getDeclaredMethod("secret");
        secretMethod.setAccessible(true);   // 突破 private 限制
        secretMethod.invoke(obj2);

        // 6. 访问并修改私有字段 name
        Field nameField = clazz.getDeclaredField("name");
        nameField.setAccessible(true);
        System.out.println("修改前的 name:" + nameField.get(obj2));
        nameField.set(obj2, "李四");
        System.out.println("修改后的 name:" + nameField.get(obj2));

        // 再次调用 study 验证修改效果
        studyMethod.invoke(obj2);
    }
}

📸 截图1:Experiment2.java 代码截图

📸 截图2:运行结果

预期输出(你的实际输出应与之类似):

无参构造创建的对象:Student{name='默认学生', age=18}
有参构造创建的对象:Student{name='张三', age=22}
张三 正在学习,年龄:22
这是一个私有方法,只能被反射调用
修改前的 name:张三
修改后的 name:李四
李四 正在学习,年龄:22

四、原理分析:setAccessible(true) 的威力

  • 常规编程private 字段和方法只能在类内部访问,外部无法直接调用或修改。

  • 反射机制:通过 setAccessible(true) 可以 关闭 Java 语言的访问检查,从而突破封装。这被称为“暴力反射”。

  • 框架中的应用:Spring 在注入依赖时,即使字段是 private 的,也能直接赋值;ORM 框架将数据库列映射到实体私有字段,都依赖此特性。

⚠️ 注意:暴力反射破坏了封装性,仅在框架底层或特殊工具中使用,业务代码中应避免。


五、实验三:反射性能对比(直接调用 vs 反射调用)

反射虽然灵活,但存在性能损耗。下面通过一个循环测试来量化差距。

重要提示:为了避免运行 Experiment3 时控制台疯狂打印,请先临时修改 Student.java 中的 study() 方法,将 System.out.println(...) 注释掉或删除。实验结束后再恢复。

修改后的 study() 方法示例:

public void study() {
    // System.out.println(name + " 正在学习,年龄:" + age);
}

然后新建 Experiment3.java

import com.reflection.demo.Student;
import java.lang.reflect.Method;

public class Experiment3 {
    public static void main(String[] args) throws Exception {
        Student stu = new Student("测试", 20);
        Method studyMethod = Student.class.getDeclaredMethod("study");
        int times = 10_000_000;   // 一千万次调用

        // 直接调用
        long start1 = System.nanoTime();
        for (int i = 0; i < times; i++) {
            stu.study();
        }
        long end1 = System.nanoTime();

        // 反射调用
        long start2 = System.nanoTime();
        for (int i = 0; i < times; i++) {
            studyMethod.invoke(stu);
        }
        long end2 = System.nanoTime();

        System.out.println("直接调用耗时:" + (end1 - start1) / 1_000_000 + " ms");
        System.out.println("反射调用耗时:" + (end2 - start2) / 1_000_000 + " ms");
    }
}

📸 截图3:Experiment3.java 代码截图

📸 截图4:运行结果

典型输出(你的实际数值可能略有不同):

直接调用耗时:45 ms
反射调用耗时:320 ms

实验完成后,记得恢复 Student.java 中的 study() 方法,去掉注释。


六、性能差异原因及优化建议

原因 说明
动态解析 反射需要在运行时解析方法签名、检查访问权限、进行类型转换。
JIT 编译限制 直接调用可以被 JIT 内联优化,反射调用则难以被优化。
安全检查 即使 setAccessible(true) 仍有一定开销。

优化建议

  • 缓存 MethodFieldConstructor 对象,避免重复调用 getDeclaredMethod

  • 在高频调用的场景(如循环)中避免使用反射。

  • 框架层面通常会缓存反射元数据,例如 Spring 的 ReflectionUtils


七、心得体会:反射是一把双刃剑

通过本次实验,我深刻体会到:

  1. 灵活性 vs 性能:反射让代码变得极其灵活,可以实现“不可能”的操作(如调用私有方法),但牺牲了运行效率。

  2. 封装性的突破setAccessible(true) 虽然强大,但滥用会导致代码脆弱、难以维护。框架作者需要谨慎使用,业务开发者则应优先使用常规 API。

  3. 理解框架基础:Spring 的依赖注入、MyBatis 的自动映射、JUnit 的测试执行,底层都大量使用反射。掌握了反射,阅读框架源码会豁然开朗。

建议:在写工具类、框架或测试代码时可以考虑反射;普通业务逻辑中,能用 new 和直接调用就尽量不用反射。


参考文献

  1. Java官方文档:java.lang.reflect 包 [Online]

  2. CSDN博客《Java反射机制(一):深入理解Class对象》

  3. Spring Framework 源码:ReflectionUtils.java


如果本文对你有帮助,欢迎 点赞 👍 + 收藏 ⭐,你的支持是我持续分享的动力。最近在网上刷到了刘晓艳老师的一个讲座,有些观点启人以思,分享给大家。

1.卡耐基曾说过:“一个人的成功因素中,70%靠人际关系,30%靠个人努力。

2.努力不重要,机会更重要。

努力提升自己,去与优秀的人为伍,然后成为他们中的一员,大家一起变得更好。

更多推荐