反射

一、Java 语言高级特性

要理解反射,不能只盯着反射本身看。反射是 Java 高级语言特性之一,和其他几个高级特性同属一层、互相配合。先看全貌,再看个体。

1.1 语言特性分两层

Java 的语言特性可以分两大类:

基础语言特性——写业务代码的主力工具:

特性 用途
class、interface、继承、多态、封装 构建业务模型
if/else、for、switch 流程控制
异常处理 错误处理
方法调用、new 对象 执行逻辑
基本类型、数组 数据载体

高级语言特性——解决框架开发、通用抽象、复杂场景的工具:

特性 解决的问题
反射 运行时操作编译期未知的类
泛型 类型参数化,让容器/方法通吃多种类型
注解 给代码贴标签,让框架识别特殊语义
动态代理 不改原方法代码,给它包一层行为
序列化 对象与字节流互转,跨进程/跨机器传递
Lambda / Stream 函数式编程,简化集合操作
并发(JUC、锁、JMM) 多线程安全与协调
NIO 高效 I/O,非阻塞网络通信
类加载器 运行时动态加载外部 .class

1.2 高级特性的共性

它们"高级"在哪?四个共同特点:

  1. 需要在运行时处理类型/行为——编译期的静态规则不够用了
  2. 面向"通用抽象"而非"具体业务"——写给所有类用的代码,不是给某个 User 类用的
  3. 是框架/库的基石——Spring、MyBatis、Jackson 全靠它们才能工作
  4. 学习曲线陡峭——需要理解底层机制(JVM、字节码、内存模型)才能用好

1.3 反射在矩阵里的位置

反射是高级特性矩阵里最基础的一块,因为其他几个都依赖它:

              高级语言特性矩阵
                    │
        ┌───────────┼───────────┐
        │           │           │
     反射(地基)   泛型       注解
        │           │           │
        │     (擦除后靠       (运行时注解
        │      反射拿类型)     靠反射读取)
        │                       │
        ├───→ 动态代理           │
        │   (JDK 代理用          │
        │    反射调方法)          │
        │                       │
        └───→ 序列化             │
            (Jackson/Gson      │
             靠反射读字段)       │
                                │
            ←──────────────────┘
            (注解 + 反射 + 动态代理
             = Spring AOP 的全套基础)
高级特性 依赖反射吗? 典型框架用例
反射 - Spring IoC:扫描类 → 反射 new → 反射 set 字段
泛型 部分(运行时擦除后要靠反射拿类型) Java 集合框架、MyBatis 泛型返回值
注解 是(运行时注解靠反射读取) JUnit 扫描 @Test、Jackson 扫描 @JsonProperty
动态代理 是(JDK 动态代理用反射调方法) Spring AOP(事务、日志)、MyBatis Mapper
序列化 是(Jackson/Gson 靠反射读字段) REST API 的 JSON 自动转换

反射是其他高级特性的"地基"——这就是为什么它被排在第一位讲。理解了反射,再看注解、动态代理、序列化,就能一眼看透它们的实现原理。反射 + 注解 + 动态代理如何构成 Spring IoC 与 AOP 的运行时基础,见 Spring Framework 核心体系


二、词源与本义

2.1 词源

Reflect 来自拉丁语 reflectere,由 re-(回)+ flectere(弯曲)组成,字面意思是"弯回来"。

在日常生活中,reflect 是"反射、映照"——镜子把光弯回来,照见自身。

在编程领域,引申为:程序在运行时"回头审视自己"——查看自己的类、字段、方法,甚至操作它们。

程序 normally 是往外看的(调用别的对象、处理外部数据),反射是程序"往内看"——审视自己的结构。这就是"反"字的含义。

2.2 一句话定义

反射 = 程序在运行时,动态地查看自己的类结构、创建对象、读写字段、调用方法的能力。


三、反射解决的根本问题

3.1 编译期的确定 vs 运行期的不确定

Java 是静态强类型语言——编译期就定死了"类有什么字段、什么方法、什么父类"。编译完的字节码就是一张静态蓝图:

// 编译期就得写死:要调 User 的 setName
User user = new User();
user.setName("张三");

这在大多数场景下没问题。但有一类程序,编译期根本不知道自己要操作什么类、调什么方法

3.2 什么场景编译期不可能知道?

三个经典例子:

例子 1:Spring 的 IoC 容器

<!-- applicationContext.xml -->
<bean id="userService" class="com.example.UserService">
    <property name="userDao" ref="userDao"/>
</bean>

Spring 启动时读 XML,看到 com.example.UserService 这个字符串类名,然后要:
1. 找到这个类并加载
2. 调用它的无参构造器 new 出实例
3. 拿到 setUserDao 方法,把 userDao 注入进去

Spring 编译的时候知道有 UserService 这个类吗?根本不知道——这是用户写的业务类,Spring 只是个框架。

如果没有反射,Spring 就是死代码。框架之所以是"框架",核心能力就是在运行时,加载和操作用户写的、它从未见过的类

例子 2:JDBC 加载驱动

Class.forName("com.mysql.cj.jdbc.Driver");

字符串 "com.mysql.cj.jdbc.Driver"——编译时 JDK 根本不知道有这个类,驱动包是运行时才加进 classpath 的。

例子 3:插件化系统(IDE、浏览器插件)

IDE 开发者写 IDEA 时,知道未来会有"Scala 插件"、"Lombok 插件"吗?不可能知道。但 IDEA 提供了插件 API,任何符合接口的 .jar 放进去,运行时就能加载、调用——这就是反射。

3.3 不用反射会怎样?

假设世界上没有反射,Spring 要怎么搞?只能是用户写好类之后,去改 Spring 的源码:

// 伪代码:没有反射的 Spring
if (beanClass.equals("com.example.UserService")) {
    UserService us = new UserService();
    us.setUserDao(userDao);
    beans.put("userService", us);
} else if (beanClass.equals("com.example.OrderService")) {
    OrderService os = new OrderService();
    beans.put("orderService", os);
}
// ... 每个业务类都加一个 else if

这样 Spring 就不是框架了——它就变成了"别人写的应用代码"。每加一个业务类就要重新编译 Spring,这根本不可行。

3.4 正向调用 vs 反射调用

"反"这个字的关键——正常代码是正向的,反射是反向的:

正向调用(编译期确定):
  写代码 user.setName("张三")
      │
      ↓
  编译器:哦,User 类有 setName(String),生成 invokevirtual 指令
      │
      ↓
  运行时直接执行

反射调用(运行期确定):
  给字符串 "setName" + 参数 "张三"
      │
      ↓
  JVM 运行时:去 User 类的方法表找名字叫 setName 的方法
      │
      ↓
  找到后:检查权限、组装参数栈、调用

正向:写代码时就知道调哪个方法,编译器拦截错误。

反向:给一个方法名字符串,运行时 JVM 查找、组装、执行。


四、反射的基石:Class 类

反射的一切能力,入口都是 java.lang.Class

4.1 Class 是什么?

每个类被 JVM 加载后,堆里会产生一个唯一的 Class<T> 对象,它就是这个类的"元数据档案"——里面存着:
- 类名、父类、实现的接口
- 所有字段(Field)
- 所有方法(Method)
- 所有构造器(Constructor)
- 所有注解

JVM 内存结构:
  ┌──────────────────────────────────────┐
  │ 堆(Heap)                           │
  │                                      │
  │  User 对象 1  ──类指针──→             │
  │  User 对象 2  ──类指针──→ Class<User> │ ← 唯一!所有 User 实例共享
  │  User 对象 3  ──类指针──→             │    (存着 User 的全部元数据)
  │                                      │
  └──────────────────────────────────────┘

每个 User 对象里都有一个隐藏字段 klass(类指针),指向同一个 Class<User> 对象。这就是为什么运行时能知道"这个对象是什么类型"。

4.2 获取 Class 对象的三种方式

// 方式一:类字面常量(编译期确定)
Class<User> c1 = User.class;

// 方式二:对象的 getClass() 方法(运行时确定,多态)
Object obj = new User();
Class<?> c2 = obj.getClass();  // 运行时才知道是 User,不是 Object

// 方式三:Class.forName(完全动态,运行时才知道类名)
Class<?> c3 = Class.forName("com.example.User");

三种方式的区别,是理解反射的关键:

方式 确定时机 是否需要 import 核心用途
User.class 编译期 需要 泛型、类型传递
obj.getClass() 运行时 不需要 多态场景下拿到实际类型
Class.forName 运行时 不需要 框架加载用户类、JDBC 驱动

三种方式拿到的 Class 对象是同一个——因为 JVM 保证每个类只有一份元数据档案。


五、反射的四大核心能力

拿到 Class 对象后,就拥有了对这个类的"上帝视角"。

5.1 能力一:查看类的结构(自省)

Class<?> c = Class.forName("com.example.User");

// 类名
c.getName();           // "com.example.User"
c.getSimpleName();     // "User"

// 父类
c.getSuperclass();     // Class<Object>

// 实现的接口
c.getInterfaces();     // [Class<Serializable>, ...]

// 所有 public 字段(含继承的)
Field[] fields = c.getFields();

// 所有声明的字段(不含继承的,但含 private)
Field[] allFields = c.getDeclaredFields();

// 所有 public 方法(含继承的)
Method[] methods = c.getMethods();

// 所有声明的方法(不含继承的,但含 private)
Method[] allMethods = c.getDeclaredMethods();

// 所有构造器
Constructor<?>[] ctors = c.getDeclaredConstructors();

5.2 能力二:动态创建对象

Class<?> c = Class.forName("com.example.User");

// 方式一:调用无参构造器
User user = (User) c.getDeclaredConstructor().newInstance();

// 方式二:调用有参构造器
Constructor<?> ctor = c.getDeclaredConstructor(String.class, int.class);
User user = (User) ctor.newInstance("张三", 28);

5.3 能力三:操作字段

Class<?> c = User.class;
User user = new User("张三", 28);

// 读 public 字段
Field nameField = c.getField("name");
Object name = nameField.get(user);  // "张三"

// 读/写 private 字段
Field ageField = c.getDeclaredField("age");
ageField.setAccessible(true);       // 关闭访问检查!突破 private
int age = (int) ageField.get(user); // 28
ageField.set(user, 29);             // 把 private 的 age 改成 29

setAccessible(true) 是反射的"后门钥匙"——它绕过 Java 的访问控制机制,private、protected、默认访问全部失效。这也是反射的争议点所在。

5.4 能力四:调用方法

Class<?> c = User.class;
User user = new User("张三", 28);

// 调用 public 方法
Method setName = c.getMethod("setName", String.class);
setName.invoke(user, "李四");  // 等价于 user.setName("李四")

// 调用 private 方法
Method calcSecret = c.getDeclaredMethod("calcSecret", int.class);
calcSecret.setAccessible(true);
Object result = calcSecret.invoke(user, 10);

5.5 四大能力速查表

能力 核心 API 操作的粒度
自省(看结构) getFields/getMethods/getConstructors 及其 Declared 变体
创建对象 Constructor.newInstance 写(新建实例)
操作字段 Field.get/set + setAccessible 读写(字段值)
调用方法 Method.invoke + setAccessible 执行(方法)

六、反射的代价

反射能力很强,但代价也极其明确——反射的每一步,都在牺牲编译期能做的优化和检查

6.1 代价一:性能开销

正常调用一个方法:

编译期就知道方法地址 → 生成 invokevirtual → 直接跳转,1 条指令

反射调用一个方法:

Class.forName → 查方法表 → 检查参数 → 装箱/拆箱参数 → 校验权限 → 构造调用栈 → 执行

性能差距:微基准下,反射调用比直接调用慢 10~100 倍(差距来自方法查找、参数装箱、JIT 无法内联)。

但要注意:绝大多数场景这个差距感知不到。Spring 里一个请求调 10 次反射,和 IO 耗时比起来微不足道。只有在**超高频循环(每秒百万次)**的场景才需要规避反射。

6.2 代价二:编译期类型安全丢失

// 正常代码:编译期拦截错误
user.setAge("28");   // 编译报错!setAge 需要 int

// 反射代码:编译期不知道 setAge 需要 int,运行时才报错
Method m = c.getMethod("setAge", String.class);   // 编译通过
m.invoke(user, "28");  // 运行时 NoSuchMethodException

"编译期 1 秒的红波浪线"变成了"运行时 1 小时的异常堆栈"。

6.3 代价三:破坏封装

setAccessible(true) 能直接读写 private 字段、调用 private 方法。这意味着:
- 类设计者设的所有 private 防线,反射一来全部归零
- 类的**不变量(Invariant)**可以被外部绕过修改
- 单例可以被反射破坏(枚举单例除外,JVM 级防反射)

// 单例被反射破坏(非枚举实现)
Constructor<Singleton> ctor = Singleton.class.getDeclaredConstructor();
ctor.setAccessible(true);
Singleton s2 = ctor.newInstance();  // new 出了第二个实例!

6.4 代价四:重构脆弱

正常代码重命名 setNamechangeName,IDE 全局重构完成修改。

反射代码里的字符串 "setName"?IDE 根本不知道这是个方法名——重构完,反射调用处运行时才报错。

字符串引用 = 重构盲区。反射用得越多,重构成本越高。

6.5 代价五:安全隐患

setAccessible 能打开后门,给了攻击者可乘之机:
- 反序列化漏洞(如 Log4j)本质上就是:攻击者控制了反序列化的类,通过反射链执行任意代码
- 模块系统的模块隔离(JPMS)就是为了限制反射的滥用

6.6 五大代价汇总

代价 说明 影响程度
性能开销 方法查找、参数装箱、JIT 无法内联 高频场景明显,常规场景可忽略
类型安全丢失 编译期不检查,运行时才报错 所有反射代码都有
破坏封装 private 防线失效,不变量可被绕过 setAccessible 时才有
重构脆弱 字符串引用是重构盲区 所有反射代码都有
安全隐患 反序列化攻击、后门打开 涉及外部输入时才有

七、什么时候用反射?

反射虽有代价,但价值在于:它是框架能成为框架的唯一手段

7.1 正确的反射使用场景

场景 典型例子 为什么非反射不可?
框架/容器 Spring IoC、MyBatis、Dubbo 框架要操作它从未见过的用户类
动态代理 JDK 动态代理 Proxy.newProxyInstance 需要动态调用被代理的方法
序列化/反序列化 Jackson、Gson、Protobuf 映射层 需要自动读写对象的所有字段
ORM Hibernate、MyBatis ResultMap 需要把数据库列自动塞进对象字段
测试工具 JUnit、Mockito 需要调用 private 方法、mock 私有字段
插件化架构 IDE 插件、浏览器扩展 动态加载运行时才出现的类
配置驱动 读取 XML/JSON 配置创建对象 类名来自配置文件,是字符串

7.2 不该用反射的场景

场景 反例 正确做法
普通业务逻辑 调用 user.setName() 用反射写了 10 行 直接调用
追求极致性能的热路径 循环 100 万次用反射设字段 用普通方法调用 / 代码生成(ASM)
需要强类型安全 接口方法调用还在用字符串拼 用接口多态
类型可以在编译期确定 已 import 该类,还写 Class.forName 直接 new / 直接调用

判断标准编译期能不能确定要操作哪个类?

  • 能 → 不要用反射
  • 不能(来自配置、来自用户、来自外部)→ 用反射

八、getXxx() vs getDeclaredXxx():别再搞混了

这是反射 API 最容易踩的坑。

API 包含什么 不包含什么
getFields() 所有 public 字段 + 从父类继承的 public 字段 非 public 字段(private/protected/default)
getDeclaredFields() 自己声明的所有字段(含 private/protected/default) 父类的任何字段
getMethods() 所有 public 方法 + 从父类继承的 public 方法 非 public 方法
getDeclaredMethods() 自己声明的所有方法(含 private/protected/default) 父类的任何方法
getConstructors() public 构造器 非 public 构造器
getDeclaredConstructors() 所有构造器(含 private) -

口诀:

带 Declared = 只看自己声明的,不管访问权限,不管父类

不带 Declared = 只要是 public,不管自己的还是父类继承的

常见翻车场景:

// 坑 1:想拿 private 字段,用了 getField,抛 NoSuchFieldException
Field f = User.class.getField("age");     // 错!age 是 private,用 getDeclaredField

// 坑 2:想拿父类的 protected 方法,用了 getDeclaredMethod,找不到
Method m = Child.class.getDeclaredMethod("parentProtected"); // 错!去父类 getDeclaredMethod
// 正确:要么拿父类 Class 调用 getDeclaredMethod,要么用 getMethod(如果是 public)

九、常见误解澄清

9.1 误解 1:"反射就是拿到类名然后 new 一下"

太浅了。反射是四大能力的集合:自省、创建、读写字段、调用方法。框架真正用到的是后三个,而且会配合注解(运行时读取注解决定行为)。

9.2 误解 2:"反射很慢,所以不要用"

以偏概全。反射慢是事实,但"慢多少"和"是否重要"取决于场景:
- Web 请求、数据库操作的耗时是毫秒级,反射多花几微秒,可以忽略
- 高频循环、批量计算的微秒级场景,反射才是瓶颈

Spring/Hibernate 全是反射,从未感觉慢——因为 IO 才是瓶颈。

9.3 误解 3:"getDeclaredXxx 就是拿 private 的"

不准确getDeclaredXxx 拿的是自己声明的所有访问级别的成员——包括 public、protected、default、private。它的反义词不是"拿 public",而是"不含继承"。

9.4 误解 4:"只有框架才用反射,业务代码永远不该用"

太绝对了。测试代码(调 private 方法、给 @Autowired 字段塞值)、临时工具类(把一个对象的非空字段拷到另一个对象)里用反射是很常见的。原则是:别在核心热路径滥用

9.5 误解 5:"枚举也能被反射破坏单例"

错误。JVM 硬编码了防护:Constructor.newInstance() 里会检查 clazz.isEnum(),如果是枚举,直接抛 IllegalArgumentException: Cannot reflectively create enum objects。这就是枚举是最佳单例实现方式的原因之一。


十、学习路径建议

第一阶段:建立概念
├── 理解 Java 语言特性的两层划分(基础 vs 高级)
├── 搞懂反射在高级特性矩阵里的定位(是其他特性的地基)
└── 理解"正向调用 vs 反射调用"的区别

第二阶段:掌握 API
├── 获取 Class 的三种方式(.class / getClass() / forName)
├── 四大能力:自省、创建、读写字段、调用方法
├── getXxx vs getDeclaredXxx 的区别
└── setAccessible 的作用与风险

第三阶段:理解原理
├── Class 对象在 JVM 里的位置(堆中的元数据档案)
├── 反射的性能瓶颈在哪里(方法查找、参数装箱、JIT 限制)
├── 反射如何破坏封装和单例
└── 枚举为什么能防反射(JVM 硬编码检查)

第四阶段:工程实践
├── 读懂 Spring IoC 的反射调用链
├── 读懂 Jackson/Gson 的反射序列化流程
├── 理解动态代理 + 反射 = AOP 的实现原理
└── 判断业务场景该不该用反射

十一、总结

反射是 Java 高级语言特性之一,是其他高级特性(注解、动态代理、序列化)的地基。

它解决的根本问题是:程序在运行时,才能知道要操作什么类——这是框架能成为框架的唯一手段。如果没有反射,所有框架都得把每个业务类的逻辑硬编码进源码,重新编译才能用。

反射的四大能力:自省(看结构)、创建(new 对象)、读写字段、调用方法。核心入口是 Class 对象,每个类在 JVM 里只有一份。

但反射是双刃剑,五大代价:性能开销、类型安全丢失、封装破坏、重构脆弱、安全隐患。

正确的使用原则:编译期能确定的,就不用反射;只有编译期确定不了的(类/方法来自配置、用户、外部),才用反射。

一句话记忆:反射是"运行时回头审视自己"的能力——程序在运行时才知道要操作什么类,就靠反射动态加载、查看、操作它。