反射概念
凌晨 2 点,你盯着屏幕上的错误日志发呆。线上 OOM 了,堆 dump 文件 2GB。运维同事扔给你一个 jar 包:「内存泄漏在这类里,自己查。」
你手里只有编译好的 class 文件,没有源码。
怎么查?
答案是:反射。Java 给你开了一扇后门,让你在运行时「照镜子」,看清类到底长什么样。
镜子里的 Java 世界
Java 是一门静态语言,代码写完就定型了。但 Class 类改变了这一点——它是 Java 对「类的描述」的描述。
不是绕口令。让我们理清关系:
class String { ... } // 这是类
String.class // 这是 Class 对象——类的「元信息」每个类加载到 JVM 时,虚拟机会自动为它生成一个唯一的 Class 对象。这个对象里存储着类的所有信息:叫什么名字、属于哪个包、有哪些字段、定义了哪些方法、继承自谁、实现哪些接口。
反射,就是通过这个 Class 对象「反向」获取这些信息的过程。
反射能做什么?
不是所有事情反射都能干。来看看它的能力边界。
可以做到的
运行时「问」类要信息,这是反射的核心能力:
java
public class ReflectionCapabilities {
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName("java.util.ArrayList");
// 问它叫什么
System.out.println("类名: " + clazz.getName()); // java.util.ArrayList
System.out.println("简单名: " + clazz.getSimpleName()); // ArrayList
// 问它属于哪个包
System.out.println("包: " + clazz.getPackage().getName()); // java.util
// 问它继承自谁
System.out.println("父类: " + clazz.getSuperclass().getName()); // java.util.AbstractList
// 问它实现了哪些接口
System.out.println("接口数量: " + clazz.getInterfaces().length);
for (Class<?> i : clazz.getInterfaces()) {
System.out.println(" - " + i.getSimpleName());
}
// 问它有哪些字段
System.out.println("字段数量: " + clazz.getDeclaredFields().length);
// 问它有哪些方法
System.out.println("方法数量: " + clazz.getDeclaredMethods().length);
}
}运行时「操控」对象,这就把能力从「看」扩展到「改」:
java
public class ReflectionManipulation {
static class SecretVault {
private String password = "123456";
private int attempts = 0;
}
public static void main(String[] args) throws Exception {
SecretVault vault = new SecretVault();
Class<?> clazz = vault.getClass();
// 读取私有字段(正常代码根本做不到)
Field passwordField = clazz.getDeclaredField("password");
passwordField.setAccessible(true); // 关键:这行代码打开了潘多拉魔盒
String password = (String) passwordField.get(vault);
System.out.println("密码是: " + password);
// 修改私有字段
passwordField.set(vault, "999999");
System.out.println("修改后: " + passwordField.get(vault));
}
}不能做到的
反射不是万能的。有些事情,它做不到:
| 能力 | 反射能做到吗 | 说明 |
|---|---|---|
| 修改 final 字段的值 | 部分可以 | 如果 final 字段是基本类型或 String 常量,JIT 编译后值会被内联,修改无效 |
| 突破模块系统限制 | JDK 9+ 受限 | 需要 --add-opens 参数显式开放模块 |
| 获取泛型具体类型 | 不完全 | 泛型在编译后被擦除,运行时只能看到 List,看不到 List<String> |
| 访问已加载类的私有成员 | 取决于上下文 | 同一模块可以,跨模块需要 --add-opens |
看一个反例:
java
public class GenericErasureDemo {
static void printType(List<String> list) {
// 运行时,list 的泛型信息已经丢失
Class<?> clazz = list.getClass();
System.out.println("实际类型: " + clazz.getName());
// 只能看到: java.util.ArrayList
// 看不到 String
}
public static void main(String[] args) {
List<String> stringList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
printType(stringList); // 输出: java.util.ArrayList
printType(intList); // 输出: java.util.ArrayList
// 两者在运行时看起来一模一样!
}
}核心类一览
反射的 API 主要在 java.lang.reflect 包里,核心类就这几个:
Member 接口(定义访问成员的通用行为)
├── Field → 字段:我有什么数据
├── Method → 方法:我能做什么
└── Constructor<T> → 构造:我怎么被创建
Modifier 类 → 工具类:解析修饰符(public/private/static/final...)
Array 类 → 工具类:动态操作数组
Proxy 类 → 工具类:动态代理理解这个层次关系很重要。Field、Method、Constructor 都实现了 Member 接口,它们有共同的行为模式。
为什么需要反射?
没有反射的世界是什么样的?
不用反射:
service.saveUser(user); // 代码里硬编码了类名
用反射:
String className = config.get("service.class"); // 从配置文件读取
Class.forName(className).newInstance().save(user); // 动态调用反射让 Java 拥有了「延迟绑定」的能力——程序可以推迟到运行时才决定使用哪个类、调用哪个方法。这是框架设计的基石。
Spring 为什么不需要你手动 new 一个 Bean?反射。 JUnit 怎么自动发现和执行测试方法?反射。 Jackson 怎么把 JSON 转成对象?反射。
要点回顾
- Class 对象是反射的入口 — 每个类在 JVM 中都有一个对应的 Class 对象
- 反射可以「看」也可以「改」 — 不仅能获取信息,还能修改字段值、调用方法
- 反射有边界 — 无法突破泛型擦除、模块系统等限制
- 反射是双刃剑 — 灵活的背后是性能开销和安全风险
反射不是银弹,但它是 Java 生态系统的隐形功臣。没有它,就没有现代框架的繁荣。
