Java 反射
运行时动态获取类信息(字段、方法、构造器)并操作对象,编译期无需知道具体类型。详解 反射。
使用场景
- Spring IOC:扫描类、注入依赖
- MyBatis:Mapper 接口代理
- 注解处理:
@Transactional、@RequestMapping - 序列化 / 动态代理
获取 Class 的三种方式
Class<User> c1 = User.class; // 类字面量
Class<?> c2 = user.getClass(); // 实例
Class<?> c3 = Class.forName("com.x.User"); // 全限定名,会触发初始化同一类加载器下,同一个 Class 对象(c1 == c2)。
| 方式 | 是否触发初始化 |
|---|---|
.class | 否 |
getClass() | 对象已创建 |
forName | 默认会初始化(forName(name, false, loader) 可不初始化) |
见 类加载与双亲委派。
核心 API
Constructor<User> c = clazz.getDeclaredConstructor(String.class);
c.setAccessible(true);
User u = c.newInstance("张三");
Method m = clazz.getDeclaredMethod("setName", String.class);
m.setAccessible(true);
m.invoke(u, "李四");
Field f = clazz.getDeclaredField("age");
f.setAccessible(true);
f.setInt(u, 20);getXxx vs getDeclaredXxx
| 方法 | 范围 |
|---|---|
getMethod / getField | public,含父类 |
getDeclaredMethod / getDeclaredField | 本类全部(含 private),不含父类 |
访问 private 需 setAccessible(true)(JDK 9+ 模块系统可能受限)。
反射与泛型
运行时 List<String> 擦除为 List,实例上拿不到元素类型;声明处可保留:
Type t = getClass().getGenericSuperclass(); // ParameterizedType
Type[] args = ((ParameterizedType) t).getActualTypeArguments();见 运行时如何拿泛型类型。
反射与注解
只有 @Retention(RetentionPolicy.RUNTIME) 的注解才能反射读取:
if (method.isAnnotationPresent(Transactional.class)) {
Transactional tx = method.getAnnotation(Transactional.class);
}| Retention | 能否反射 |
|---|---|
| SOURCE | 否 |
| CLASS | 否 |
| RUNTIME | 是 |
优缺点
| 优点 | 缺点 |
|---|---|
| 框架扩展、解耦 | 性能低于直接调用 |
| 动态加载插件 | 破坏封装(访问 private) |
| 注解驱动 | 编译期无法检查类型安全 |
性能开销:方法查找、装箱、权限检查。优化:缓存 Method/Field、setAccessible(true);高频路径用 CGLIB/ByteBuddy 生成字节码。
常见面试题
Q:反射为什么慢?
A:字符串查找成员、可访问性检查、native/装箱;可缓存 Method 并关闭检查。
Q:Spring 如何用反射?
A:扫描 @Component、解析构造器/@Autowired 注入、处理 @PostConstruct、JDK/CGLIB 代理调用方法。
Q:.class 和 Class.forName 区别?
A:后者指定类加载器加载并默认初始化类(执行 static 块)。
Q:能否反射修改 final 字段?
A:技术上可能(setAccessible + 去 final 修饰),但破坏语义;String 等不应依赖此 hack。
Q:反射和动态代理关系?
A:JDK 动态代理基于 InvocationHandler + 反射调用;CGLIB 用子类继承,目标方法仍可能反射调用。