Skip to content

反射基础 ​

S1-023建议学进阶约 90 分钟JDK 21编程语言基础 · 常用类

1. 本节定位 ​

反射(Reflection)让你在运行时检查类的结构、创建对象、读写字段、调用方法——这些操作平时都由编译器帮你完成。

它是"框架为什么能自动干活"的答案:Spring 靠它注入依赖,MyBatis 靠它把查询结果映射成对象,Jackson 靠它序列化。理解反射,是从"会用框架"走向"看懂框架"的门槛。

2. 学习目标 ​

完成本节后,你应该能够:

  • 说出反射解决的核心问题(运行时操作类信息);
  • 用三种方式获取 Class 对象,并读取类名、父类、字段、方法;
  • 用反射创建实例、读写字段(含私有字段)、调用方法;
  • 解释 setAccessible(true) 在做什么,以及它为什么有风险;
  • 说出反射的代价,并判断"什么时候该用反射、什么时候不该用";
  • 认出 ClassNotFoundException 与 NoClassDefFoundError 的区别。

3. 前置知识检查 ​

4. 为什么需要这个知识 ​

先看一个矛盾:框架要在不知道你的类长什么样的情况下操作它。

java
// 你自己的代码:编译期就知道类型
User user = new User("张三");
user.getName();

// 框架的代码:编译时它根本不知道有 User 这个类
// 它只有配置文件里的一行:com.example.User
// 它必须"在运行时"加载这个类 → 创建实例 → 赋值 → 调用方法

没有反射,框架就只能靠代码生成或手工配置。有了反射,Spring 才能只凭一个类名完成依赖注入。

另一个视角:反射让"编译期检查"失效了——method.invoke(obj) 拼错方法名不会编译报错,只在运行时抛异常。所以它是一把双刃剑:框架用它换灵活性,业务代码用它通常是给自己挖坑。

5. 核心概念 ​

5.1 Class 对象:一切反射的入口 ​

每个被 JVM 加载的类,都有一个对应的 Class 对象(在内存里只有一份)。三种获取方式:

java
Class<Product> byLiteral = Product.class;                              // 1. 类字面量,编译期已知
Class<?> byInstance = new Product().getClass();                        // 2. 从实例获取
Class<?> byName = Class.forName("cn.kb.learn.common.Product");         // 3. 按全限定名加载(框架用这个)
方式时机典型场景
X.class编译期已知类型,最安全
obj.getClass()运行时拿到对象后想知道它到底是什么类
Class.forName("全限定名")运行时配置文件驱动(框架的用法)

三种方式得到的是同一个对象:byLiteral == byInstance 为 true。因为类在 JVM 里只有一个 Class 实例。

5.2 能读到什么 ​

java
clazz.getName();                       // 全限定名 cn.kb.learn.common.Product
clazz.getSimpleName();                 // 简单名 Product
clazz.getSuperclass();                 // 父类
clazz.getInterfaces();                 // 实现的接口
clazz.getModifiers();                  // 修饰符(配合 Modifier.toString)
clazz.getDeclaredFields();             // 本类声明的字段(含私有)
clazz.getDeclaredMethods();            // 本类声明的方法(含私有)
clazz.getDeclaredConstructors();       // 构造方法

getDeclaredXxx 与 getXxx 的区别(高频考点):

方法范围访问权限
getDeclaredFields()本类声明的包含 private
getFields()本类 + 父类的只有 public

5.3 创建实例 ​

java
// 方式一:无参构造(已过时,异常处理粗糙)
Product instance = Product.class.newInstance();

// 方式二:显式取构造方法(推荐)
Constructor<Product> constructor = Product.class.getConstructor(String.class, long.class);
Product created = constructor.newInstance("键盘", 19_900L);

方式二更好:能指定参数类型、异常信息更明确,也能通过 getDeclaredConstructor() 访问非公开构造方法。

5.4 操作字段与方法 ​

java
Field nameField = Product.class.getDeclaredField("name");
nameField.setAccessible(true);          // 关闭访问检查
nameField.set(instance, "机械键盘");     // 写
Object value = nameField.get(instance); // 读

Method describe = Product.class.getMethod("describe");
Object result = describe.invoke(instance);       // 调用

Method secret = Product.class.getDeclaredMethod("secret");
secret.setAccessible(true);
secret.invoke(instance);                          // 调用私有方法

setAccessible(true) 在做什么:跳过 Java 的访问权限检查。它是打破封装的操作——所以:

  • 业务代码里几乎不该出现(除非在写框架、序列化器、测试工具);
  • Java 9 之后模块系统会限制跨模块的深度反射(JDK 内部类可能直接拒绝);
  • Java 9+ 提供了 trySetAccessible():返回 boolean 而不是抛异常,更适合"不确定能否访问"的场景。

5.5 反射的代价 ​

代价说明
性能方法调用比直接调用慢(实测约 5 倍,见第 7 节),因为要做访问检查、参数装箱、方法解析
失去编译期检查方法名、参数类型写错,编译不报错,运行时才抛异常
代码可读性invoke 调用看不出调了什么,IDE 无法跳转
破坏封装setAccessible(true) 可以读写任何字段,包括 final 的
安全性在受限环境(安全管理器)下会被拒绝

判断标准:需要"在不知道具体类型的情况下操作对象"时才用反射。如果你的代码里能写出 obj.getName(),就不要用 invoke。

5.6 什么时候真的需要反射 ​

场景说明
框架/容器Spring 的依赖注入、MyBatis 的结果映射、Jackson 的序列化
通用工具Bean 拷贝、对象比较、ORM 的实体映射
注解处理读取注解并据此执行逻辑(《注解基础》)
插件系统按配置加载实现类(Class.forName)
单元测试测试私有方法(通常有更好的选择)

业务代码里用反射的典型信号:你在写框架层或通用工具层。普通的 CRUD 业务里出现反射,通常意味着设计出了问题。

6. 递进示例 ​

6.1 最小示例:读取类信息 ​

java
Class<?> clazz = Product.class;
System.out.println(clazz.getName());          // cn.kb.learn...Product
System.out.println(clazz.getSimpleName());    // Product
System.out.println(clazz.getSuperclass().getSimpleName());   // Object

6.2 业务示例:按配置创建对象 ​

java
// 配置文件里写:handler=cn.kb.learn.handler.OrderHandler
String className = "cn.kb.learn.common.ReflectionLesson$Product";
Class<?> clazz = Class.forName(className);
Object instance = clazz.getDeclaredConstructor().newInstance();

这就是"插件化"的最小形态:换配置就能换实现,不用改代码、不用重新编译。

6.3 反例:滥用反射 ​

java
// 反例一:明明编译期就知道类型,却用反射
User user = (User) Class.forName("com.example.User").getDeclaredConstructor().newInstance();
// 直接 new User() 更安全、更快、IDE 能检查

// 反例二:用反射访问私有字段(破坏封装)
Field field = User.class.getDeclaredField("password");
field.setAccessible(true);
// 现在你可以读写任何字段,包括本该被保护的

// 反例三:用反射调私有方法做单元测试
// 测试应该测"公开行为",而不是依赖内部实现细节

7. 完整可运行代码 ​

文件:labs/common-classes/src/main/java/cn/kb/learn/common/ReflectionLesson.java(节选核心部分,完整代码见仓库)

java
package cn.kb.learn.common;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;

public class ReflectionLesson {
    public static class Product {
        public static final String CATEGORY = "默认分类";

        private String name;
        private long priceInCents;

        public Product() {
            this.name = "未命名";
        }

        public Product(String name, long priceInCents) {
            this.name = name;
            this.priceInCents = priceInCents;
        }

        public String describe() {
            return name + " / " + priceInCents + " 分";
        }

        /** 仅通过反射调用(getDeclaredMethod + setAccessible),编译期看不到引用 */
        @SuppressWarnings("unused")
        private String secret() {
            return "私有方法被反射调用";
        }

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

    public static void main(String[] args) throws Exception {
        Class<Product> byLiteral = Product.class;
        Class<?> byInstance = new Product().getClass();
        Class<?> byName = Class.forName("cn.kb.learn.common.ReflectionLesson$Product");
        System.out.println("三种方式得到同一个 Class: "
                + (byLiteral == byInstance && byInstance == byName));
        System.out.println("全限定名: " + byLiteral.getName());
        System.out.println("简单名: " + byLiteral.getSimpleName());

        System.out.println("父类: " + byLiteral.getSuperclass().getSimpleName());
        System.out.println("修饰符: " + Modifier.toString(byLiteral.getModifiers()));
        System.out.println("声明的字段: " + Arrays.toString(
                Arrays.stream(byLiteral.getDeclaredFields()).map(Field::getName).sorted().toArray()));
        System.out.println("声明的方法: " + Arrays.toString(
                Arrays.stream(byLiteral.getDeclaredMethods()).map(Method::getName).sorted().toArray()));

        Constructor<Product> constructor = byLiteral.getConstructor(String.class, long.class);
        Product created = constructor.newInstance("键盘", 19_900L);
        System.out.println("反射创建实例: " + created);

        Field nameField = byLiteral.getDeclaredField("name");
        nameField.setAccessible(true);
        nameField.set(created, "机械键盘");
        System.out.println("反射修改私有字段后: " + created);

        Method describe = byLiteral.getMethod("describe");
        System.out.println("调用公开方法: " + describe.invoke(created));

        Method secret = byLiteral.getDeclaredMethod("secret");
        secret.setAccessible(true);
        System.out.println("调用私有方法: " + secret.invoke(created));

        Field category = byLiteral.getDeclaredField("CATEGORY");
        System.out.println("静态字段: " + category.get(null));

        int times = 200_000;
        long start = System.nanoTime();
        for (int i = 0; i < times; i++) {
            describe.invoke(created);
        }
        long reflectCost = (System.nanoTime() - start) / 1_000_000;

        start = System.nanoTime();
        for (int i = 0; i < times; i++) {
            created.describe();
        }
        long directCost = (System.nanoTime() - start) / 1_000_000;

        System.out.println("调用 " + times + " 次 —— 反射: " + reflectCost + " ms,直接调用: " + directCost + " ms");
    }
}

运行命令:

powershell
cd labs
./mvnw -pl common-classes compile
java -cp common-classes/target/classes cn.kb.learn.common.ReflectionLesson

预期输出:

text
三种方式得到同一个 Class: true
全限定名: cn.kb.learn.common.ReflectionLesson$Product
简单名: Product
父类: Object
修饰符: public static
声明的字段: [CATEGORY, name, priceInCents]
声明的方法: [describe, secret, toString]
反射创建实例: Product{name='键盘', priceInCents=19900}
反射修改私有字段后: Product{name='机械键盘', priceInCents=19900}
调用公开方法: 机械键盘 / 19900 分
调用私有方法: 私有方法被反射调用
静态字段: 默认分类
调用 200000 次 —— 反射: 63 ms,直接调用: 11 ms

四处值得注意:

输出含义
三种方式得到同一个 Class: true类在 JVM 里只有一份 Class 对象
声明的字段: [CATEGORY, name, priceInCents]包含私有字段与静态常量
反射修改私有字段后: ...'机械键盘'私有字段被成功修改——封装被打破
最后一行:63 ms vs 11 ms反射比直接调用慢约 5 倍(数值依机器而变,关注数量级)

8. Debug 观察点 ​

  1. 在 byLiteral == byInstance 处断点,观察两个变量的 id 相同——同一个 Class 对象;
  2. 在 nameField.set(created, "机械键盘") 处按 F7 进入,观察它最终走到 Unsafe 或字段偏移量的写入——这就是"绕过访问控制"的实现层面;
  3. 在 describe.invoke(created) 处按 F7 进入,观察 Method.invoke 内部的参数校验与装箱过程,理解性能开销来自哪里;
  4. 把 getDeclaredField("name") 改成 getField("name"),观察抛出的 NoSuchFieldException(因为 getField 只找 public 字段)——一次实验记住两者的区别。

9. 常见错误 ​

错误 1:ClassNotFoundException 与 NoClassDefFoundError 分不清 ​

text
ClassNotFoundException        ← 你主动调用 Class.forName(字符串写错了)
NoClassDefFoundError          ← JVM 要用这个类,但 classpath 里没有

两者的区别在 《Java 语言与运行机制》 讲过:前者是"主动找没找到",后者是"被动用发现缺失"。

错误 2:setAccessible(true) 在模块系统下失败 ​

text
InaccessibleObjectException: Unable to make field private final ... accessible

原因:Java 9+ 的模块系统限制了对 JDK 内部类的深度反射。修复:不要反射 JDK 内部类;确有必要时用 --add-opens 启动参数(并明白这会带来兼容性风险)。

错误 3:方法名写错,运行时才炸 ​

java
Product.class.getMethod("getname");      // NoSuchMethodException:大小写错了

修复:反射代码必须有测试覆盖,因为编译器帮不了你。

错误 4:在业务代码里滥用反射 ​

见 6.3 节。修复:能用接口 + 多态解决的问题,不要用反射。反射是框架的工具,不是业务代码的工具。

10. 动手实验 ​

目标:用反射写一个极简的"对象转字符串"工具。

步骤:

  1. 运行第 7 节代码,核对输出(重点看最后两行的时间对比);
  2. 写方法 String describe(Object target):用反射输出对象的类名与所有字段名值对(如 Product{name=机械键盘, priceInCents=19900});
  3. 让它支持 private 字段(用 setAccessible(true)),并说明这在真实项目里是否合适;
  4. 写方法 Object create(String className):按类名创建实例(无参构造),类不存在时抛出带清晰消息的异常;
  5. 用第 4 步的方法创建 Product、String(无无参构造,应失败)各一次,观察异常差异。

验收方式:能解释第 3 步"为什么反射能读私有字段",以及第 5 步 String 为什么会失败。

扩展任务:给第 2 步的 describe 加上"跳过静态字段"与"跳过 null 字段"的逻辑(提示:Modifier.isStatic(field.getModifiers()))。

11. 练习题 ​

基础题

  1. 判断:getFields() 能获取到父类的私有字段。(对 / 错)
  2. 填空:获取 Class 对象的三种方式是 、、______。
  3. 简答:反射最主要的两个代价是什么?

代码阅读题

java
class Item {
    private String name = "默认";

    private String name() {
        return name;
    }
}

Object item = Item.class.getDeclaredConstructor().newInstance();
Method method = Item.class.getDeclaredMethod("name");
System.out.println(method.invoke(item));
  1. 这段代码能运行吗?如果不能,需要加什么?

编程题

  1. 写一个方法 Map<String, Object> toMap(Object target):
    • 用反射把对象的所有非静态字段转成 Map<字段名, 字段值>;
    • 跳过值为 null 的字段;
    • 处理 IllegalAccessException(转换成 IllegalStateException,保留原因)。

综合任务

  1. 实现一个极简的"依赖注入容器":
    • 定义注解 @Inject(可用于字段);
    • 写方法 Object createAndInject(Class<?> type):
      • 创建目标类实例;
      • 遍历字段,凡是标注了 @Inject 的,就创建该字段类型的实例并注入;
    • 用它组装一个 OrderService(依赖 OrderRepository),并打印注入结果;
    • 说明:Spring 的依赖注入本质上做了什么?为什么它能"自动"装配?
分级提示

提示 1(方向):第 4 题的两个方法都是 private,getDeclaredMethod 能找到,但调用前需要打开访问权限。

提示 2(关键点):第 5 题的字段遍历用 getDeclaredFields(),跳过 Modifier.isStatic(...) 的字段。

提示 3(接近答案):第 6 题的注入流程是"创建实例 → 遍历字段 → 有 @Inject 就 new 一个字段类型 → setAccessible(true) → set 进去",这正是 Spring 早期版本的核心逻辑。

12. 参考答案 ​

基础题
  1. 错。getFields() 只返回 public 字段(包括继承来的);要拿本类的所有字段(含 private)用 getDeclaredFields()。父类的私有字段任何方法都拿不到(getSuperclass().getDeclaredFields() 可以拿父类自己声明的)。
  2. X.class、obj.getClass()、Class.forName("全限定名")。
  3. ① 性能开销(访问检查、装箱、方法解析,通常慢数倍);② 失去编译期检查(写错方法名/参数类型只在运行时暴露),此外还有破坏封装与安全限制的问题。
代码阅读题(第 4 题)

不能运行,会抛 IllegalAccessException。

getDeclaredMethod("name") 能找到私有方法,但 invoke 前必须打开访问权限:

java
Method method = Item.class.getDeclaredMethod("name");
method.setAccessible(true);          // 关键的一行
System.out.println(method.invoke(item));   // 默认

顺带一个注意点:Item 是包级私有类(没有 public),如果测试代码在别的包,连 getDeclaredConstructor() 都需要 setAccessible(true)。

编程题(第 5 题)
java
package cn.kb.learn.common;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.LinkedHashMap;
import java.util.Map;

public class ObjectMapper {
    static Map<String, Object> toMap(Object target) {
        if (target == null) {
            throw new IllegalArgumentException("目标对象不能为 null");
        }
        Map<String, Object> result = new LinkedHashMap<>();
        for (Field field : target.getClass().getDeclaredFields()) {
            if (Modifier.isStatic(field.getModifiers())) {
                continue;
            }
            field.setAccessible(true);
            try {
                Object value = field.get(target);
                if (value != null) {
                    result.put(field.getName(), value);
                }
            } catch (IllegalAccessException exception) {
                throw new IllegalStateException("无法读取字段: " + field.getName(), exception);
            }
        }
        return result;
    }

    record User(String name, Integer age) {
    }

    public static void main(String[] args) {
        System.out.println(toMap(new User("张三", 30)));
        System.out.println(toMap(new User("李四", null)));
    }
}

预期输出(record 的字段是 private final,反射可读):

text
{name=张三, age=30}
{name=李四}
综合任务(第 6 题)
java
package cn.kb.learn.common;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;

public class MiniContainer {
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.FIELD)
    @interface Inject {
    }

    static class OrderRepository {
        String describe() {
            return "订单仓储已就绪";
        }
    }

    static class OrderService {
        @Inject
        private OrderRepository repository;

        String handle() {
            return "服务调用: " + repository.describe();
        }
    }

    static Object createAndInject(Class<?> type) throws Exception {
        Object instance = type.getDeclaredConstructor().newInstance();
        for (Field field : type.getDeclaredFields()) {
            if (field.isAnnotationPresent(Inject.class)) {
                Object dependency = field.getType().getDeclaredConstructor().newInstance();
                field.setAccessible(true);
                field.set(instance, dependency);
            }
        }
        return instance;
    }

    public static void main(String[] args) throws Exception {
        OrderService service = (OrderService) createAndInject(OrderService.class);
        System.out.println(service.handle());
    }
}

Spring 的依赖注入本质上做了什么:

  1. 扫描:找到标注了注解的类与字段(反射 + 注解);
  2. 实例化:用构造方法创建对象(反射);
  3. 注入:把依赖对象塞进字段(Field.set + setAccessible)或构造参数;
  4. 管理生命周期:把对象放进容器(单例池),统一管理创建与销毁。

为什么能"自动"装配:因为注解告诉它"这里需要一个依赖",而反射让它能在运行时不写死类型地创建与注入。这也解释了为什么"改一个注解就能换实现"——所有信息都在运行时由框架读取。

预期输出:

text
服务调用: 订单仓储已就绪

13. 掌握标准 ​

  • [ ] 能用三种方式获取 Class 对象并读取类信息;
  • [ ] 能用反射创建实例、读写字段、调用方法(含私有成员);
  • [ ] 知道 setAccessible(true) 在做什么,以及它的风险;
  • [ ] 能说出 getDeclaredXxx 与 getXxx 的区别;
  • [ ] 能判断"什么时候该用反射、什么时候该用接口与多态"。

14. 扩展资源 ​

15. 下一步学习 ​

《注解基础》:注解负责"声明意图",反射负责"执行意图"——两者合起来才是 Spring 的完整面貌。

进度只保存在本机浏览器,可随时清除

个人学习项目:AI 生成内容均经人工复核,第三方资源仅作索引