第 33 章:反射基础
学习目标
- 理解反射是什么、为什么框架都靠它
- 掌握 Class / Field / Method / Constructor 四大 API
- 了解反射的性能代价
一、什么是反射?
正常写代码:编译期就知道类名、方法名。
java
User user = new User();
user.setName("张三");反射:运行期才知道类名、方法名(从配置文件、注解里读出来)。
java
Class<?> clazz = Class.forName("com.taskflow.User"); // ① 字符串 → Class
Object user = clazz.getDeclaredConstructor().newInstance();
Method m = clazz.getMethod("setName", String.class);
m.invoke(user, "张三");为什么框架都需要反射? Spring 不可能知道你会写什么类。它扫描到
@Service注解后,只有一个类名字符串,必须靠反射把对象造出来。
二、获取 Class 对象的三种方式
java
// ① 类名.class —— 编译期确定,最快
Class<User> c1 = User.class;
// ② 对象.getClass() —— 有实例时用
Class<?> c2 = user.getClass();
// ③ Class.forName("全限定名") —— 运行期动态,框架最常用
Class<?> c3 = Class.forName("com.taskflow.User");
System.out.println(c1 == c2 && c2 == c3); // true,同一个 Class 对象为什么是同一个? 每个类在 JVM 中只有一份
Class对象,由类加载器保证(同一个 ClassLoader 下)。
三、创建对象
java
Class<?> clazz = Class.forName("com.taskflow.User");
// ✅ JDK 9+ 推荐写法
Object obj = clazz.getDeclaredConstructor().newInstance();
// 带参构造
Constructor<?> con = clazz.getDeclaredConstructor(String.class, int.class);
Object user = con.newInstance("张三", 18);
// ❌ clazz.newInstance() 已废弃(JDK 9+):会吞掉构造器抛出的受检异常四、操作字段
java
Field field = clazz.getDeclaredField("name"); // ① getDeclaredField 能拿到 private
field.setAccessible(true); // ② 关闭访问检查,才能操作 private
field.set(user, "李四"); // ③ 赋值
String name = (String) field.get(user); // ④ 取值| 方法 | 范围 |
|---|---|
getField(name) | 只能拿 public(含父类继承的) |
getDeclaredField(name) | 本类所有(含 private),不含父类 |
getFields() | 所有 public |
getDeclaredFields() | 本类所有 |
setAccessible(true)就是 Spring 能给你的private字段注入@Autowired的原理。
五、调用方法
java
// ① 获取方法:方法名 + 参数类型列表
Method method = clazz.getDeclaredMethod("setName", String.class);
method.setAccessible(true);
// ② 调用:invoke(实例, 参数...)
method.invoke(user, "王五");
// ③ 静态方法:实例传 null
Method staticM = clazz.getDeclaredMethod("create");
staticM.invoke(null);
// ④ 有返回值
Object result = clazz.getMethod("getName").invoke(user);六、读取注解
java
@Retention(RetentionPolicy.RUNTIME) // ① 必须是 RUNTIME,反射才读得到
@Target(ElementType.TYPE)
public @interface Table {
String value();
}
@Table("t_user")
public class User { }
// 读取
Table table = User.class.getAnnotation(Table.class);
System.out.println(table.value()); // t_user这就是 MyBatis-Plus 的
@TableName("t_user")能映射表名的原理。
七、完整实战:迷你 IOC 容器
java
public class MiniContainer {
private final Map<Class<?>, Object> beans = new HashMap<>();
/** 注册一个 bean:反射创建实例 */
public void register(Class<?> clazz) throws Exception {
Object instance = clazz.getDeclaredConstructor().newInstance(); // ①
beans.put(clazz, instance);
}
/** 依赖注入:扫描 @Inject 字段并赋值 */
public void inject() throws Exception {
for (Object bean : beans.values()) {
for (Field field : bean.getClass().getDeclaredFields()) { // ②
if (field.isAnnotationPresent(Inject.class)) { // ③ 有注解才注入
field.setAccessible(true);
Object dep = beans.get(field.getType()); // ④ 按类型找依赖
field.set(bean, dep); // ⑤ 注入
}
}
}
}
public <T> T get(Class<T> clazz) {
return clazz.cast(beans.get(clazz));
}
}这 20 行就是 Spring IOC 的核心思想:扫描 → 反射创建 → 反射注入。剩下的是生命周期、循环依赖、AOP 代理等工程化细节。
八、反射的代价
| 问题 | 说明 |
|---|---|
| 慢 | 无法内联优化,比直接调用慢 10~100 倍 |
| 破坏封装 | setAccessible(true) 能改 private,绕过设计约束 |
| 编译期无检查 | 方法名写错,运行时才炸 |
| JDK 17 模块限制 | 反射访问 JDK 内部类需要 --add-opens |
优化手段:
java
// ✅ 缓存 Method 对象,避免重复查找(getMethod 本身很慢)
private static final Map<String, Method> CACHE = new ConcurrentHashMap<>();
Method m = CACHE.computeIfAbsent("setName",
k -> clazz.getDeclaredMethod(k, String.class));规约:业务代码不要用反射;框架代码用反射时必须缓存 Method / Field。
九、本章小结
| 要点 | 关键 |
|---|---|
| 反射 | 运行期动态操作类 |
| Class 获取 | .class / getClass() / forName() |
| 创建对象 | getDeclaredConstructor().newInstance() |
| private 访问 | setAccessible(true) |
| 注解读取 | 注解必须 @Retention(RUNTIME) |
| 性能 | 慢,框架里必须缓存 |
动手练习
练习 1:基础题
写一个 toMap(Object obj) 方法,用反射把任意对象的所有字段转成 Map<String, Object>。
练习 2:进阶题
实现一个简易 ORM:定义 @Table / @Column 注解,用反射生成 INSERT INTO t_user (name, age) VALUES (?, ?) SQL 语句。
下一章:第 34 章:动态代理 →