一、 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 反射的主要应用场景

理解反射的应用场景是回答面试问题的关键。反射广泛应用于:

  1. IDE工具:如Eclipse、IntelliJ IDEA,利用反射可感知任意一个类,从而提供代码补全、类型检查等功能。
  2. 框架开发:Spring的IoC/DI容器通过反射实例化Bean;MyBatis通过反射将ResultSet映射为Java对象。
  3. 动态代理:JDK动态代理和CGLIB代理均依赖反射机制实现。
  4. 单元测试: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 反射性能低的原因

  1. 动态解析:反射需要在运行时解析类、方法、字段,无法进行静态优化。
  2. 安全检查:每次反射调用都需要进行访问权限检查(即使设置 setAccessible(true),JVM仍需处理相关逻辑)。
  3. 装箱拆箱:反射方法的参数和返回值均为 Object 类型,涉及频繁的装箱拆箱操作。

5.2 优化策略

缓存Method对象

避免重复调用 getMethod() 或 getDeclaredMethod(),将获取到的 Method 对象缓存到Map中。

关闭访问检查

调用 method.setAccessible(true) 一次,后续调用可跳过安全检查,显著提升性能。

使用MethodHandle

JDK 7引入的 MethodHandle 比传统反射更快,它直接操作字节码,避免了部分JVM安全检查。

避免反射

在已知类型的情况下,优先使用直接调用或接口实现,而非反射。

5.3 常见面试真题

Q1
反射能否获取私有构造函数?如何实例化?

可以。通过 clazz.getDeclaredConstructor() 获取私有构造函数,然后调用 setAccessible(true),再调用 newInstance() 即可实例化。

Q2
反射能访问父类的私有方法吗?

不能。getDeclaredMethod() 只能获取当前类声明的方法,不包括父类。若需获取父类私有方法,需通过 clazz.getSuperclass().getDeclaredMethod() 递归查找。

Q3
什么是反射的安全管理器(SecurityManager)?

在安全管理器启用时,反射操作可能受到限制。例如,访问其他包的私有成员可能需要特定的权限。在生产环境中,通常不启用安全管理器,但在高安全要求场景下需注意。

六、 总结

Java反射是Java语言动态特性的核心,掌握其原理和应用是成为高级Java工程师的必经之路。在面试中,不仅要会写反射代码,更要理解其底层机制、性能影响及在框架中的应用。建议结合Spring、MyBatis等源码进行深入学习,方能融会贯通。

本文涵盖了反射的基础概念、Class对象获取、方法调用、动态代理、注解处理及性能优化等核心考点,希望能帮助求职者构建完整的知识体系。如需更多深入解析,请持续关注 kaoacca.cn。