反射
一、Java 语言高级特性
要理解反射,不能只盯着反射本身看。反射是 Java 高级语言特性之一,和其他几个高级特性同属一层、互相配合。先看全貌,再看个体。
1.1 语言特性分两层
Java 的语言特性可以分两大类:
基础语言特性——写业务代码的主力工具:
| 特性 | 用途 |
|---|---|
| class、interface、继承、多态、封装 | 构建业务模型 |
| if/else、for、switch | 流程控制 |
| 异常处理 | 错误处理 |
| 方法调用、new 对象 | 执行逻辑 |
| 基本类型、数组 | 数据载体 |
高级语言特性——解决框架开发、通用抽象、复杂场景的工具:
| 特性 | 解决的问题 |
|---|---|
| 反射 | 运行时操作编译期未知的类 |
| 泛型 | 类型参数化,让容器/方法通吃多种类型 |
| 注解 | 给代码贴标签,让框架识别特殊语义 |
| 动态代理 | 不改原方法代码,给它包一层行为 |
| 序列化 | 对象与字节流互转,跨进程/跨机器传递 |
| Lambda / Stream | 函数式编程,简化集合操作 |
| 并发(JUC、锁、JMM) | 多线程安全与协调 |
| NIO | 高效 I/O,非阻塞网络通信 |
| 类加载器 | 运行时动态加载外部 .class |
1.2 高级特性的共性
它们"高级"在哪?四个共同特点:
- 需要在运行时处理类型/行为——编译期的静态规则不够用了
- 面向"通用抽象"而非"具体业务"——写给所有类用的代码,不是给某个 User 类用的
- 是框架/库的基石——Spring、MyBatis、Jackson 全靠它们才能工作
- 学习曲线陡峭——需要理解底层机制(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 代价四:重构脆弱
正常代码重命名 setName → changeName,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 里只有一份。但反射是双刃剑,五大代价:性能开销、类型安全丢失、封装破坏、重构脆弱、安全隐患。
正确的使用原则:编译期能确定的,就不用反射;只有编译期确定不了的(类/方法来自配置、用户、外部),才用反射。
一句话记忆:反射是"运行时回头审视自己"的能力——程序在运行时才知道要操作什么类,就靠反射动态加载、查看、操作它。