第一部分:反射的本质------程序如何"照镜子"?
你必须理解反射的哲学定义:在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性。
1. 为什么叫"反射"?
正常情况下,我们是根据类名(编译期已知)去创建对象,这叫"正向"。
而反射是在程序运行过程中,根据一个字符串或一个 Class 对象,去探索这个类的内部结构,这就像是程序在运行时"照镜子"观察自己。
2. 反射的优缺点
-
优点 :赋予了程序极强的动态性。它是插件化架构、注解处理器、动态代理的灵魂。
-
缺点:
-
性能开销:反射涉及类型匹配、安全检查等,比直接的 Java 调用慢。
-
安全问题 :反射可以打破封装(访问
private字段),可能破坏对象内部的一致性。
-
第二部分:反射的核心基石------java.lang.Class
在 JVM 中,每个类被加载后,都会产生一个唯一的 Class 对象。它是反射的入口。
1. 获取 Class 对象的三种方式
Java
// 方式一:类名.class(最安全,性能最好,编译期已知)
Class<User> clazz1 = User.class;
// 方式二:对象.getClass()(运行期已知对象)
User user = new User();
Class<? extends User> clazz2 = user.getClass();
// 方式三:Class.forName("全限定类名")(最灵活,配置化首选)
Class<?> clazz3 = Class.forName("com.demo.entity.User");
第三部分:反射的操作手册------属性、方法、构造器
通过 Class 对象,我们可以"肢解"一个类。
1. 构造器(Constructor):暴力创建对象
即使构造函数是 private,反射也能强行实例化。
Java
Constructor<User> constructor = User.class.getDeclaredConstructor(String.class);
// 关键:暴力反射,打破封装
constructor.setAccessible(true);
User user = constructor.newInstance("张三");
2. 字段(Field):操作隐私数据
面试官:"反射能修改 final 字段吗?"
深度回答 :可以,但有条件。对于编译器已经优化的常量(如 static final String s = "abc"),反射修改后的结果可能无法在程序中体现,因为编译器可能已经把值内联了。
3. 方法(Method):动态调用
Java
Method method = clazz.getDeclaredMethod("sayHello", String.class);
method.setAccessible(true);
// invoke 第一个参数是实例对象,后面是方法参数
Object result = method.invoke(user, "World");
第四部分:反射的高级进阶------越过泛型检查
这是大厂面试中的经典场景:如何在一个 ArrayList<Integer> 中添加一个 String?
原理 :Java 的泛型是"伪泛型",只在编译期有效(类型擦除)。运行时的 ArrayList 实际上存的是 Object。
Java
List<Integer> list = new ArrayList<>();
list.add(10);
// 获取 list 运行时的 Class 对象
Method addMethod = list.getClass().getDeclaredMethod("add", Object.class);
// 绕过编译期检查,直接在运行期注入
addMethod.invoke(list, "我是反射混进来的字符串");
System.out.println(list); // 输出:[10, 我是反射混进来的字符串]
第五部分:实战场景------手写一个简单的 IoC 容器
为了理解反射在 Spring 中的应用,我们来看这个简化版的 Bean 注入:
Java
public class SimpleIoC {
private Map<String, Object> beans = new HashMap<>();
public void loadBeans(String className) throws Exception {
Class<?> clazz = Class.forName(className);
Object instance = clazz.getDeclaredConstructor().newInstance();
// 模拟自动注入:寻找带有 @MyAutowired 注解的字段
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(MyAutowired.class)) {
field.setAccessible(true);
// 这里简化逻辑,直接从 beans map 中获取并注入
field.set(instance, beans.get(field.getType().getName()));
}
}
beans.put(className, instance);
}
}
第六部分:反射性能优化------不再是"慢"的代名词
面试官:"既然反射慢,为什么 Spring 还要大规模使用?"
1. 优化策略
-
缓存中间结果 :不要每次调用都
getDeclaredMethod,应该将Method对象缓存起来。 -
ReflectASM:大厂常用高性能库,通过字节码生成技术(直接生成调用类的子类)来规避反射。
-
setAccessible(true):关闭安全检查可以显著提升性能。
第七部分:面试复盘脑图
Code snippet
mindmap
root((Java 反射机制))
核心入口
Class 对象: 类的元数据映射
三种获取方式: .class, getClass(), forName()
操作维度
Constructor: 实例化对象 (支持私有)
Field: 读写属性 (打破 final 限制)
Method: 动态调用方法 (invoke 机制)
底层原理
类加载过程: Loading -> Linking -> Initialization
类型擦除: 运行时泛型信息丢失, 反射可绕过
实战应用
框架基石: Spring IoC, MyBatis, JUnit
动态代理: JDK 动态代理依赖反射调用
注解处理: 运行时解析自定义注解
性能与安全
安全检查: setAccessible(true) 的双刃剑
优化手段: 结果缓存, 字节码增强 (CGLIB/ASM)
第八部分:大厂面试官的"夺命连环炮"
-
getDeclaredMethods()和getMethods()有什么区别?- 回答要点 :
getMethods()返回该类及其所有父类 的 public 方法;getDeclaredMethods()返回该类声明的所有方法,包括 private、protected,但不包括父类方法。
- 回答要点 :
-
反射创建对象和 new 创建对象有什么区别?
- 回答要点 :
new是编译期确定的,属于强引用,编译器会检查类型。反射是运行期确定的,属于动态加载,能实现解耦,但性能略低且缺乏编译期安全检查。
- 回答要点 :
-
在双亲委派模型下,反射是如何找到类的?
- 回答要点 :
Class.forName默认使用调用者 的类加载器(Caller's ClassLoader)。在复杂环境下(如 Tomcat 或 OSGi),如果加载器不对,反射会抛出ClassNotFoundException。
- 回答要点 :
结语:从"API 调用者"到"架构设计者"
反射是 Java 语言从静态向动态跨越的桥梁。 只有真正掌握了反射,你才能看懂 Spring 的 BeanDefinition,才能理解为什么 MyBatis 需要一个空的构造函数,才能在遇到复杂 Bug 时,通过逆向分析字节码找到根源。
这篇文章总结了反射最硬核的考点。如果你能结合这些知识点,在面试中自信地聊聊反射在代理模式(JDK Proxy)中的运用,那么高级开发的 Offer 已经是你的囊中之物。