一、 Java反射基础概念
Java反射(Reflection)是Java语言的一个重要特性,它允许程序在运行时(Run-time)自我检查(Self-inspection)和修改内部属性。在面试中,面试官通常会首先询问反射的基本定义及其在框架中的应用。
1.1 什么是反射?
反射机制是指在运行时动态获取类的信息(如字段、方法、构造函数等)并动态调用对象的方法或访问字段的能力。简而言之,反射让Java具备了“动态性”,使得我们可以在不知道具体类的情况下操作对象。
1.2 反射的核心API
Java反射的核心类位于 java.lang.reflect 包中,主要包括以下几个类:
- Class<T>:代表类的实体,在运行的Java应用程序中表示类和接口。
- Field:代表类的成员变量(成员变量也称为类的属性)。
- Method:代表类的方法。
- Constructor:代表类的构造方法。
- Array:提供动态创建数组和访问数组元素的方法。
1.3 反射的主要应用场景
理解反射的应用场景是回答面试问题的关键。反射广泛应用于:
- IDE工具:如Eclipse、IntelliJ IDEA,利用反射可感知任意一个类,从而提供代码补全、类型检查等功能。
- 框架开发:Spring的IoC/DI容器通过反射实例化Bean;MyBatis通过反射将ResultSet映射为Java对象。
- 动态代理:JDK动态代理和CGLIB代理均依赖反射机制实现。
- 单元测试:JUnit通过反射查找并执行标注了@Test注解的方法。
二、 获取Class对象的三种方式
在Java中,任何类在加载时都会在方法区生成一个对应的 Class 对象。面试中常问:“如何获取一个类的Class对象?”
通过类名.class获取
Class<String> clazz = String.class;
这是最安全、最简单的获取方式。它在编译时就被确定,不会抛出 ClassNotFoundException。适用于在编译期已知类名的情况。
通过对象.getClass()获取
String str = new String("Hello");
Class<?> clazz = str.getClass();
这种方式需要知道具体的对象实例。由于 getClass() 返回的是 Class<?> 类型,可能需要类型转换或通配符处理。适用于运行时已知对象实例的情况。
通过Class.forName()获取
Class<?> clazz = Class.forName("java.lang.String");
这是最常用的一种方式,尤其在框架开发中。它通过类的全限定名字符串获取Class对象,并且会触发类的初始化(执行静态代码块)。适用于运行时通过配置文件或参数动态指定类名的情况。
注意: Class.forName() 会加载并初始化类,而 ClassLoader.loadClass() 仅加载不初始化,这是面试中的常见陷阱。
三、 反射调用方法与动态代理
反射的强大之处在于能够动态调用方法。这一特性是理解Spring AOP和JDK动态代理的基础。
3.1 获取并调用方法
要动态调用一个方法,首先需要获取 Method 对象,然后调用 invoke() 方法。
import java.lang.reflect.Method;
public class ReflectionDemo {
public static void main(String[] args) throws Exception {
Class<?> clazz = Class.forName("com.example.User");
Object obj = clazz.getDeclaredConstructor().newInstance();
// 获取私有方法
Method method = clazz.getDeclaredMethod("privateMethod", String.class);
// 取消访问检查
method.setAccessible(true);
// 调用方法
Object result = method.invoke(obj, "argument");
System.out.println(result);
}
}
关键点:
getMethod()只能获取 public 方法(包括继承的)。getDeclaredMethod()可以获取类中所有声明的方法(包括 private),但不包括继承的方法。- 调用私有方法前必须设置
setAccessible(true),否则会抛出IllegalAccessException。
3.2 JDK动态代理原理
JDK动态代理是反射的重要应用。它通过 java.lang.reflect.Proxy 类和 InvocationHandler 接口,在运行时动态生成代理类。
public class MyInvocationHandler implements InvocationHandler {
private Object target;
public MyInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Before method: " + method.getName());
Object result = method.invoke(target, args); // 核心:反射调用目标方法
System.out.println("After method: " + method.getName());
return result;
}
}
面试常问:JDK动态代理和CGLIB有什么区别?
| 特性 | JDK动态代理 | CGLIB |
|---|---|---|
| 实现原理 | 反射机制 | 字节码生成(ASM) |
| 代理对象 | 必须实现接口 | 可以是普通类 |
| 性能 | JDK 7/8 性能较好,JDK 9+ 大幅提升 | 早期性能较好,现代JVM下两者差距缩小 |
| 依赖 | JDK内置 | 需引入第三方库 |
四、 反射与注解(Annotation)处理
注解本身没有行为,它只是元数据。反射机制赋予了注解“生命力”,使得框架能够根据注解执行特定逻辑。Spring的 @Autowired、MyBatis的 @Select 均依赖此机制。
4.1 自定义注解与反射解析
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyAnnotation {
String value() default "";
}
public class AnnotationDemo {
@MyAnnotation(value = "test")
public void doSomething() {
System.out.println("Doing something");
}
}
// 解析代码
Method method = AnnotationDemo.class.getMethod("doSomething");
if (method.isAnnotationPresent(MyAnnotation.class)) {
MyAnnotation annotation = method.getAnnotation(MyAnnotation.class);
System.out.println(annotation.value()); // 输出: test
}
4.2 生命周期(Retention)的影响
面试中常问:为什么有些注解无法通过反射获取?
- SOURCE:注解仅存在于源码中,编译后丢弃(如
@Override)。 - CLASS:注解保留在Class文件中,但JVM加载时丢弃(如
@Deprecated)。 - RUNTIME:注解保留在Class文件中,并加载到JVM中,可通过反射获取(如
@Autowired)。
因此,若要通过反射获取注解,必须使用 RetentionPolicy.RUNTIME。
五、 反射性能优化与面试题
反射虽然强大,但性能较低。在高频调用场景下(如RPC框架、序列化库),反射的性能开销不可忽视。面试官常问:“如何优化反射性能?”
5.1 反射性能低的原因
- 动态解析:反射需要在运行时解析类、方法、字段,无法进行静态优化。
- 安全检查:每次反射调用都需要进行访问权限检查(即使设置
setAccessible(true),JVM仍需处理相关逻辑)。 - 装箱拆箱:反射方法的参数和返回值均为
Object类型,涉及频繁的装箱拆箱操作。
5.2 优化策略
避免重复调用 getMethod() 或 getDeclaredMethod(),将获取到的 Method 对象缓存到Map中。
调用 method.setAccessible(true) 一次,后续调用可跳过安全检查,显著提升性能。
JDK 7引入的 MethodHandle 比传统反射更快,它直接操作字节码,避免了部分JVM安全检查。
在已知类型的情况下,优先使用直接调用或接口实现,而非反射。
5.3 常见面试真题
可以。通过 clazz.getDeclaredConstructor() 获取私有构造函数,然后调用 setAccessible(true),再调用 newInstance() 即可实例化。
不能。getDeclaredMethod() 只能获取当前类声明的方法,不包括父类。若需获取父类私有方法,需通过 clazz.getSuperclass().getDeclaredMethod() 递归查找。
在安全管理器启用时,反射操作可能受到限制。例如,访问其他包的私有成员可能需要特定的权限。在生产环境中,通常不启用安全管理器,但在高安全要求场景下需注意。
六、 总结
Java反射是Java语言动态特性的核心,掌握其原理和应用是成为高级Java工程师的必经之路。在面试中,不仅要会写反射代码,更要理解其底层机制、性能影响及在框架中的应用。建议结合Spring、MyBatis等源码进行深入学习,方能融会贯通。
本文涵盖了反射的基础概念、Class对象获取、方法调用、动态代理、注解处理及性能优化等核心考点,希望能帮助求职者构建完整的知识体系。如需更多深入解析,请持续关注 kaoacca.cn。