深入理解 Java 反射:从 Class 对象到方法调用(含性能实测 + 迷你 IOC 实战 + 面试考点)
📌 引言
Spring 能靠一个 @Autowired 就把对象塞给你,MyBatis 能把数据库结果集自动映射成实体类,JUnit 能找到你写的 @Test 方法并调用它------这些框架背后都站着同一个东西:反射(Reflection)。
但反射也是最容易被误用的技术。有人面试能背出"反射就是运行期获取类的信息",却说不清 getMethod 和 getDeclaredMethod 的区别;有人图省事在循环里写 Class.forName(),把接口 RT 从 10ms 拖到 800ms;还有人不知道 JDK 9 之后 setAccessible(true) 对 JDK 内部类已经不管用了。
本文从 Class 对象讲起,把反射的 API 全貌、真实使用场景、实测性能数据、四条优化路径、以及五个必须知道的坑一次讲透,最后整理面试高频考点。
一、反射的本质:运行期的"自省"
1.1 编译期 vs 运行期
普通 Java 代码是静态的:调哪个方法、访问哪个字段,编译期就写死在字节码里了。
java
User user = new User();
user.setName("张三"); // 编译期就知道要调 setName
反射是动态的:程序运行时才知道要操作哪个类的哪个成员。
java
Class<?> clazz = Class.forName("com.example.User");
Object user = clazz.getDeclaredConstructor().newInstance();
Method setName = clazz.getMethod("setName", String.class);
setName.invoke(user, "张三"); // 运行期才确定调用目标
💡 一句话理解:反射让程序在运行时能够"看见"并"操作"自己的结构。编译期不可知的类名、方法名,都可以用字符串在运行时动态解析。
1.2 Class 对象:一切反射的入口
JVM 加载一个类时,会在方法区(JDK 8+ 是元空间)为它创建一个 java.lang.Class 对象。这个对象是反射的唯一入口 ------反射的几十个 API 全都挂在 Class 上。
Class 对象里存的不是你的业务数据,而是这个类的元信息:
-
类名、包名、父类、实现的接口
-
所有字段(
Field)、方法(Method)、构造器(Constructor) -
注解、泛型签名、修饰符
┌──────────────────────────────┐ │ Class<User> (元空间) │ ├──────────────────────────────┤ │ name = "com.example.User" │ superclass = Object │ │ fields = [name, age] │──► Field 对象 │ methods = [setName, ...] │──► Method 对象 │ constructors = [User()] │──► Constructor 对象 │ annotations = [@Entity] │ └──────────────────────────────┘
1.3 获取 Class 的四种方式(面试必问区别)
| 方式 | 代码 | 是否触发类初始化(执行 static 块) |
|---|---|---|
| 类字面量 | User.class |
不会 |
| 对象获取 | user.getClass() |
对象已存在,类必然已初始化 |
Class.forName |
Class.forName("com.example.User") |
会 (默认 initialize=true) |
| 类加载器 | loader.loadClass("com.example.User") |
不会(只加载不初始化) |
java
public class ClassInitDemo {
static class Foo {
static { System.out.println("Foo 的 static 块执行了"); }
}
public static void main(String[] args) throws Exception {
System.out.println("--- Foo.class ---");
Class<?> c1 = Foo.class; // 不打印
System.out.println("--- Class.forName ---");
Class<?> c2 = Class.forName("ClassInitDemo$Foo"); // 打印
System.out.println("--- ClassLoader.loadClass ---");
Class<?> c3 = ClassInitDemo.class.getClassLoader()
.loadClass("ClassInitDemo$Foo"); // 不打印
}
}
输出:
--- Foo.class ---
--- Class.forName ---
Foo 的 static 块执行了
--- ClassLoader.loadClass ---
⚠️ 这个区别很实用 :如果只想读注解(比如扫描组件),用
Class.forName(name, false, loader)或loadClass避免触发静态初始化,能避免很多"类初始化时序"的诡异 Bug。JDBC 驱动注册就是经典案例------所以Class.forName("com.mysql.cj.jdbc.Driver")必须用默认的initialize=true,否则static块里的DriverManager.registerDriver()不会执行。
二、核心 API 全景
2.1 Constructor:创建实例
java
Class<?> clazz = User.class;
// 无参构造
Constructor<?> noArg = clazz.getConstructor();
// 指定参数类型的构造(public)
Constructor<?> withArgs = clazz.getConstructor(String.class, int.class);
// 包括 private 构造(getDeclared* 才能拿到非 public 的)
Constructor<?> priv = clazz.getDeclaredConstructor(String.class);
priv.setAccessible(true); // 取消访问检查,否则 private 构造会抛 IllegalAccessException
Object u1 = noArg.newInstance();
Object u2 = withArgs.newInstance("张三", 18);
Object u3 = priv.newInstance("李四");
newInstance() 和 Class.newInstance() 的区别(重要):
Class.newInstance()(已废弃) |
Constructor.newInstance()(推荐) |
|
|---|---|---|
| 异常包装 | 直接抛受检异常(绕过编译器检查) | 统一包装成 InvocationTargetException |
| 访问权限 | 只能调 public 无参构造 |
配合 setAccessible 可调任意构造 |
| 推荐度 | ❌ JDK 9 起 @Deprecated |
✅ 官方推荐 |
2.2 Field:读写字段
java
Class<?> clazz = User.class;
Object user = clazz.getDeclaredConstructor().newInstance();
// 拿单个字段
Field nameField = clazz.getDeclaredField("name");
nameField.setAccessible(true); // private 字段必须
nameField.set(user, "张三");
Object v = nameField.get(user);
// 遍历所有字段(含 private)
for (Field f : clazz.getDeclaredFields()) {
System.out.println(f.getName() + " : " + f.getType().getSimpleName()
+ " 修饰符=" + Modifier.toString(f.getModifiers()));
}
// 遍历所有 public 字段(含父类继承来的)
for (Field f : clazz.getFields()) {
System.out.println("public 字段: " + f.getName());
}
⚠️
getFields()vsgetDeclaredFields()是最常见的混淆点:
getFields()/getMethods()/getConstructors()→ 只返回 public ,且包含父类继承的;getDeclaredFields()/getDeclaredMethods()→ 返回本类声明的所有访问级别 (private也能拿到),但不包含父类的。
2.3 Method:调用方法
java
Class<?> clazz = User.class;
Object user = clazz.getDeclaredConstructor().newInstance();
Method setName = clazz.getMethod("setName", String.class);
setName.invoke(user, "张三");
Method getName = clazz.getMethod("getName");
Object name = getName.invoke(user);
// 调用 private 方法
Method secret = clazz.getDeclaredMethod("secretCompute", int.class);
secret.setAccessible(true);
Object result = secret.invoke(user, 42);
三个必须知道的细节:
- 参数类型要精确匹配 :
getMethod("setAge", int.class),不能写Integer.class,也不能省略(无参必须完全不传)。 invoke的异常会被包装 :目标方法内抛出的异常会被包进InvocationTargetException,必须getTargetException()取原始异常。invoke返回Object:基本类型会自动装箱,取值时要拆箱。
java
try {
method.invoke(target, args);
} catch (InvocationTargetException e) {
// 真正的业务异常在这里
Throwable real = e.getTargetException();
throw new RuntimeException("调用 " + method.getName() + " 失败", real);
}
2.4 泛型与数组:反射能拿到的"被擦除的信息"
泛型在运行时被擦除,但类/字段/方法签名上的泛型信息保留在字节码里 ,可以通过 getGenericType() 拿到:
java
public class GenericDemo {
private List<String> names;
private Map<String, Integer> scores;
}
Field f = GenericDemo.class.getDeclaredField("scores");
ParameterizedType pt = (ParameterizedType) f.getGenericType();
System.out.println(pt); // java.util.Map<java.lang.String, java.lang.Integer>
System.out.println(Arrays.toString(pt.getActualTypeArguments()));
// [class java.lang.String, class java.lang.Integer]
这就是 MyBatis、Jackson 能反序列化泛型集合的原理。
数组的反射要用 java.lang.reflect.Array:
java
Object arr = Array.newInstance(int.class, 5); // 相当于 new int[5]
Array.set(arr, 0, 100);
int v = Array.getInt(arr, 0);
System.out.println(arr.getClass().getName()); // [I
System.out.println(Array.getLength(arr)); // 5
三、实战:反射能干什么
3.1 案例一:通用 Bean 拷贝(不用 BeanUtils)
java
public class BeanCopier {
/** 把 src 中同名同类型的字段拷贝到 dest */
public static void copy(Object src, Object dest) {
if (src == null || dest == null) return;
Class<?> srcClass = src.getClass();
Class<?> destClass = dest.getClass();
for (Field srcField : srcClass.getDeclaredFields()) {
if (Modifier.isStatic(srcField.getModifiers())) continue; // 跳过 static
Field destField;
try {
destField = destClass.getDeclaredField(srcField.getName());
} catch (NoSuchFieldException e) {
continue; // dest 没有这个字段,跳过
}
if (!destField.getType().equals(srcField.getType())) continue; // 类型不兼容
try {
srcField.setAccessible(true);
destField.setAccessible(true);
destField.set(dest, srcField.get(src));
} catch (IllegalAccessException e) {
throw new IllegalStateException("拷贝字段失败: " + srcField.getName(), e);
}
}
}
public static void main(String[] args) throws Exception {
User src = new User();
src.setName("张三");
src.setAge(18);
UserVO vo = new UserVO();
copy(src, vo);
System.out.println(vo.getName() + ", " + vo.getAge());
}
}
💡 生产建议 :这个玩具版本只用于理解原理。真实项目请用
MapStruct(编译期生成代码,零反射)或Spring BeanUtils(已做 Method 缓存)。自己写反射拷贝,性能会比 MapStruct 差一到两个数量级。
3.2 案例二:迷你 IOC 容器(注解 + 反射)
这是 Spring 的核心思路,用 50 行代码就能复刻:
java
/** 自定义注入注解 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Inject {}
/** 被管理的服务 */
public class UserRepository {}
public class UserService {
@Inject
private UserRepository userRepository;
public void doWork() {
System.out.println("注入结果: " + (userRepository != null ? "成功" : "失败"));
}
}
/** 迷你容器 */
public class MiniContainer {
private final Map<Class<?>, Object> beans = new HashMap<>();
/** 注册:反射创建实例 */
public void register(Class<?> clazz) throws Exception {
Constructor<?> ctor = clazz.getDeclaredConstructor();
ctor.setAccessible(true);
beans.put(clazz, ctor.newInstance());
}
/** 注入:扫描 @Inject 字段,按类型从容器里取依赖塞进去 */
public void inject() throws Exception {
for (Object bean : beans.values()) {
for (Field field : bean.getClass().getDeclaredFields()) {
if (!field.isAnnotationPresent(Inject.class)) continue;
Object dependency = beans.get(field.getType());
if (dependency == null) {
throw new IllegalStateException("找不到依赖: " + field.getType().getName());
}
field.setAccessible(true);
field.set(bean, dependency);
}
}
}
public <T> T get(Class<T> clazz) {
return clazz.cast(beans.get(clazz));
}
public static void main(String[] args) throws Exception {
MiniContainer container = new MiniContainer();
container.register(UserRepository.class);
container.register(UserService.class);
container.inject();
container.get(UserService.class).doWork(); // 注入结果: 成功
}
}
Spring 的 @Autowired 本质上就是这三步:扫描注解 → 找到字段类型 → field.set(bean, dependency)。 差别只在于 Spring 处理了循环依赖、代理、多实现类的歧义、生命周期回调这些工程细节。
3.3 案例三:框架里的反射都在干什么
| 框架 | 反射用来做什么 |
|---|---|
| Spring | 扫描 @Component 创建 Bean;@Autowired 注入依赖;@Transactional 生成代理 |
| MyBatis | 把 ResultSet 的列按 setter 名映射到实体字段(getDeclaredMethod("set" + 列名)) |
| Jackson | 序列化时读字段/getter,反序列化时找构造器或 setter |
| JUnit 5 | 扫描 @Test 方法并调用;用 @BeforeEach 方法做前置 |
| Tomcat | 按 web.xml / 注解加载 Servlet 类并实例化 |
结论:凡是"约定优于配置"的框架,底层几乎都是反射 + 注解。
四、性能实测:反射到底慢多少
网上常见"反射慢 10 倍""慢 100 倍"的说法,但差异巨大取决于怎么用。我在本机 JDK 23 上做了实测(500 万次调用,预热充分):
java
public class ReflectBench {
public static class Target {
public int add(int a, int b) { return a + b; }
}
static final int N = 5_000_000;
public static void main(String[] args) throws Throwable {
Target t = new Target();
int sink = 0;
long s, e;
// 预热
for (int i = 0; i < 3_000_000; i++) { sink += t.add(i, 1); }
Method m = Target.class.getMethod("add", int.class, int.class);
for (int i = 0; i < 500_000; i++) { sink += (int) m.invoke(t, i, 1); }
// ① 直接调用
s = System.nanoTime();
int sum = 0;
for (int i = 0; i < N; i++) { sum += t.add(i, 1); }
e = System.nanoTime();
System.out.printf("direct call : %8.2f ns/op%n", (e - s) / (double) N);
// ② 每次重新 getMethod(最坏情况)
int M = N / 50;
s = System.nanoTime();
sum = 0;
for (int i = 0; i < M; i++) {
Method mm = Target.class.getMethod("add", int.class, int.class);
sum += (int) mm.invoke(t, i, 1);
}
e = System.nanoTime();
System.out.printf("getMethod + invoke : %8.2f ns/op%n", (e - s) / (double) M);
// ③ 缓存 Method 后调用
s = System.nanoTime();
sum = 0;
for (int i = 0; i < N; i++) { sum += (int) m.invoke(t, i, 1); }
e = System.nanoTime();
System.out.printf("cached Method.invoke : %8.2f ns/op%n", (e - s) / (double) N);
// ④ MethodHandle
MethodHandle mh = MethodHandles.lookup()
.findVirtual(Target.class, "add",
MethodType.methodType(int.class, int.class, int.class));
s = System.nanoTime();
sum = 0;
for (int i = 0; i < N; i++) { int v = (int) mh.invokeExact(t, i, 1); sum += v; }
e = System.nanoTime();
System.out.printf("MethodHandle.invokeExact: %8.2f ns/op%n", (e - s) / (double) N);
}
}
实测结果:
direct call : 1.11 ns/op
getMethod + invoke : 521.64 ns/op ← 慢 470 倍
cached Method.invoke : 19.62 ns/op ← 慢 18 倍
MethodHandle.invokeExact: 4.34 ns/op ← 慢 4 倍
4.1 数据怎么读
| 方式 | ns/op | 相对开销 | 结论 |
|---|---|---|---|
| 直接调用 | 1.11 | 1× | 基准 |
| 每次 getMethod | 521.64 | 470× | ⚠️ 绝对不能出现在循环里 |
| 缓存 Method | 19.62 | 18× | 可用,但热路径要谨慎 |
| MethodHandle | 4.34 | 3.9× | 高频场景的首选 |
三条结论:
getMethod()本身比invoke()贵得多 (找方法要在元数据里做字符串匹配)。循环里每次都getMethod是性能自杀------这是最常见的反射性能事故。- 缓存
Method后,开销降到 18 倍,绝大多数业务场景完全可接受。 MethodHandle只慢 4 倍,接近直接调用,适合框架底层。
4.2 为什么反射慢
| 开销来源 | 说明 |
|---|---|
| 访问检查 | 每次 invoke 都要校验调用方是否有权限(setAccessible(true) 可跳过) |
| 参数数组 | invoke(Object obj, Object... args) 每次都要新建 Object[],基本类型还要装箱 |
| 拆装箱 | int → Integer → 目标方法的 int,两次转换 |
| 方法查找 | getMethod 要做字符串匹配 + 遍历继承链 |
| 内联受阻 | 反射调用目标对 JIT 不透明,难以内联优化(这是最大的一笔账) |
💡 关于
Method.invoke的"inflation"机制 :JDK 并不是一直用 native 方式调用。前 15 次(sun.reflect.inflationThreshold)用NativeMethodAccessorImpl,超过阈值后 JVM 会动态生成字节码 (GeneratedMethodAccessor)来加速。所以短时间压测会有"越来越快"的现象,这也是为什么基准测试必须充分预热。
4.3 四条优化路径
java
// ① 缓存 Method ------ 收益最大、改动最小
private static final Map<String, Method> METHOD_CACHE = new ConcurrentHashMap<>();
Method method = METHOD_CACHE.computeIfAbsent(key,
k -> clazz.getMethod(name, paramTypes));
// ② setAccessible(true) ------ 跳过访问检查,约省 20~30%
method.setAccessible(true);
// ③ MethodHandle ------ 高频调用场景,接近直接调用
MethodHandle mh = MethodHandles.lookup()
.findVirtual(Target.class, "add", MethodType.methodType(int.class, int.class, int.class));
// ④ 终极方案:LambdaMetafactory 生成函数式接口(零反射开销)
// 这也是 Spring 6 / MyBatis 3.5+ / Jackson 的高性能路径
💡 最省事的方案其实是不用反射 :DTO 转换用 MapStruct,对象拷贝用编译期生成的代码。反射适合"启动期一次性工作"(扫描、注册、注入),不适合"运行期每次请求"。
五、五个必须知道的坑
5.1 setAccessible(true) 在 JDK 9+ 的模块系统下可能失效
JDK 9 引入模块系统后,对 JDK 内部类 (java.lang、java.util 等)的深度反射会被拒绝:
java.lang.reflect.InaccessibleObjectException:
Unable to make field private final byte[] java.lang.String.value accessible:
module java.base does not "opens java.lang" to unnamed module
解决办法:启动参数加 --add-opens
bash
java --add-opens java.base/java.lang=ALL-UNNAMED -jar app.jar
⚠️ 自己写的类(应用类路径上的类)不受影响,
setAccessible(true)依然有效。受限制的只有 JDK 自己的模块。
5.2 异常被包装成 InvocationTargetException
java
try {
method.invoke(target);
} catch (InvocationTargetException e) {
Throwable real = e.getTargetException(); // 一定要取这个
log.error("业务方法抛异常", real);
}
直接 catch (Exception e) 打印栈会看到一堆反射框架的调用栈,把真正的业务异常埋在中间------排查线上问题时这是噩梦。
5.3 反射能击穿单例、破坏封装
java
// 反射击穿普通单例(私有构造 + 静态实例)
Constructor<Singleton> ctor = Singleton.class.getDeclaredConstructor();
ctor.setAccessible(true);
Singleton another = ctor.newInstance(); // 拿到第二个实例!
防护方案 :用枚举实现单例 ------反射无法创建枚举实例(Constructor.newInstance 对枚举会直接抛 IllegalArgumentException),这是《Effective Java》推荐枚举单例的核心理由。
5.4 反射拿不到"运行时泛型"
java
List<String> list = new ArrayList<>();
list.getClass() == ArrayList.class; // true,泛型被擦除
// 拿不到 String 这个信息
只有在字段声明、方法签名、类继承 这些"写在字节码里"的位置才有泛型信息,getGenericType() 才能用。
5.5 反射让编译期检查失效
java
Method m = clazz.getMethod("typoMethod"); // 编译通过,运行期才报错
建议 :反射调用的地方一定要包一层 try-catch 并打日志;能不用字符串写方法名就不用。
六、面试高频考点与标准答案
1. 反射是什么?有什么用?
答:反射是 Java 在运行期 动态获取类信息并操作类成员的能力。核心是 Class 对象。用途:框架的依赖注入、ORM 映射、JSON 序列化、注解处理、动态代理、单元测试框架。
2. 获取 Class 对象的四种方式,有什么区别?
答:User.class(不初始化)、user.getClass()(已初始化)、Class.forName("...")(会初始化 ,执行 static 块)、classLoader.loadClass("...")(只加载不初始化)。
3. getMethod 和 getDeclaredMethod 的区别?
答:getMethod 只返回 public 方法(含父类继承的);getDeclaredMethod 返回本类声明的所有访问级别 方法(private 也能拿),但不含父类 。Field、Constructor 同理。
4. 反射为什么慢?慢在哪?
答:① 每次 invoke 要做访问权限检查;② invoke 的参数是 Object[],有数组创建和基本类型装箱;③ getMethod 要做字符串匹配;④ 反射调用对 JIT 不透明,无法内联------这是最大开销。实测:直接调用 1.11ns,缓存 Method 后 19.62ns,每次重新 getMethod 达 521ns。
5. 怎么优化反射性能?
答:① 缓存 Method/Field/Constructor 对象 (收益最大);② setAccessible(true) 跳过访问检查;③ 高频路径用 MethodHandle(实测只慢 4 倍);④ 极致性能用 LambdaMetafactory 生成函数式接口;⑤ 根本上:不要在循环/热路径用反射。
6. Class.newInstance() 和 Constructor.newInstance() 有什么区别?
答:Class.newInstance() 已废弃,只能调 public 无参构造,且会直接抛受检异常(绕过编译器检查)。Constructor.newInstance() 支持任意构造(配合 setAccessible),异常统一包装成 InvocationTargetException。
7. 反射能修改 final 字段吗?
答:分情况,而且有个反直觉的坑:
- 普通类的 final 实例字段 :可以改(
setAccessible(true)+Field.set); record的 final 字段 :不行 ,会抛IllegalAccessException: Can not set final int field ...(JDK 16 引入的record对此限制更严);- 改了也未必"生效" :直接访问该字段的代码可能已被常量折叠或 JIT 优化,读到的仍是旧值。我实测把
final int v = 1改成 999 后,holder.v仍返回1,只有field.get(holder)返回999。
java
final class Holder { final int v = 1; }
Holder h = new Holder();
Field f = Holder.class.getDeclaredField("v");
f.setAccessible(true);
f.set(h, 999);
System.out.println(h.v); // 1 ← 直接访问被优化,读到的还是旧值
System.out.println(f.get(h)); // 999 ← 反射读才是真实值
所以"反射能改 final"只对普通类的实例字段 成立,而且改完不一定能被普通代码看到 。这也是很多框架改用
Unsafe、或者干脆要求目标字段非 final 的原因。
8. JDK 9 之后反射有什么变化?
答:模块系统(JPMS)限制了对 JDK 内部类的深度反射 ,会抛 InaccessibleObjectException。解决方法是启动参数加 --add-opens java.base/java.lang=ALL-UNNAMED。自己的类不受影响。
9. 反射能拿到泛型信息吗?
答:运行时的对象上拿不到 (泛型被擦除),但字段声明、方法签名、类继承上的泛型 保留在字节码里,可以用 Field.getGenericType() 拿到 ParameterizedType 再取 getActualTypeArguments()。Jackson/MyBatis 的泛型反序列化就靠这个。
10. 枚举单例为什么比普通单例安全?
答:Constructor.newInstance() 对枚举类型会直接抛 IllegalArgumentException: Cannot reflectively create enum objects,所以反射无法创建第二个枚举实例 ;同时枚举的序列化由 JVM 保证反序列化后仍是同一个实例(不需要写 readResolve)。这两点让枚举成为最安全的单例实现。
七、总结
反射的定位一句话说清:
反射是"框架的地基",不是"业务的手段"。
把它记成四条:
- 入口是
Class:四种获取方式的关键差异在是否触发类初始化。 - API 分两套 :
getXxx只拿 public + 含继承;getDeclaredXxx拿全部但不含继承。 - 性能差在用法 :缓存 Method 后只慢 18 倍(可接受);每次
getMethod慢 470 倍(不可接受)。 - 边界要知道:JDK 9+ 模块限制、异常包装、泛型擦除、final 字段内联、封装被击穿。
实践建议:
- 启动期做一次的事(扫描注解、注册 Bean、建立映射)→ 放心用反射;
- 每次请求都要走的路(字段拷贝、参数绑定)→ 加缓存,或用
MethodHandle/ MapStruct; - 循环里写
Class.forName或getMethod→ 立刻改掉。
如果本文对你有帮助,欢迎点赞 + 收藏 + 关注!如果疑问或补充,欢迎在评论区交流~
关注我,后续分享更多 Java 核心知识点和实战技巧!