Java的反射机制和常见面试题

146 阅读7分钟

Java 反射(Reflection)是 Java 语言的核心特性之一,它允许程序在运行时获取类的信息并动态操作类或对象的属性和方法,而无需在编译期知道具体的类信息。这种动态性使得 Java 程序更加灵活,也是许多主流框架(如 Spring、MyBatis)的底层实现基础。

一、反射的核心原理

Java 程序运行时,每个类都会被 JVM 加载为一个Class对象(字节码对象),这个对象包含了该类的所有信息(类名、父类、方法、字段等)。反射的本质就是通过操作这个Class对象,实现对类的动态访问和控制。

简单来说:

  • 编译期:我们写的代码被编译为字节码(.class 文件)
  • 运行期:JVM 加载字节码,生成Class对象,反射通过这个对象 "反推" 出类的结构

二、反射的核心类(位于java.lang.reflect包)

反射的操作主要依赖以下核心类:

类名作用
Class表示类的字节码对象,反射的入口
Constructor表示类的构造方法,用于创建对象
Method表示类的方法,用于调用方法
Field表示类的成员变量,用于访问 / 修改属性
Modifier解析类 / 方法 / 字段的修饰符(如 public、private)

三、反射的基本操作步骤

反射的使用通常遵循以下流程:

  1. 获取目标类的Class对象(反射的入口)
  2. 通过Class对象获取需要操作的ConstructorMethodField
  3. 调用相应的 API 进行动态操作(创建对象、调用方法、访问属性等)

1. 获取Class对象的三种方式

Class对象是反射的起点,获取它有三种常用方式:

java

运行

// 方式1:通过类名.class(编译期已知类)
Class<?> cls1 = User.class;

// 方式2:通过对象.getClass()(已有对象实例)
User user = new User();
Class<?> cls2 = user.getClass();

// 方式3:通过Class.forName("全类名")(编译期未知类,最常用)
Class<?> cls3 = Class.forName("com.example.User"); // 需处理ClassNotFoundException

注意:一个类在 JVM 中只有一个Class对象,上述三种方式获取的是同一个对象。

2. 通过反射创建对象

通过反射创建对象有两种方式,推荐使用ConstructornewInstance()(更灵活,支持带参构造):

java

运行

Class<?> cls = User.class;

// 方式1:使用Class的newInstance()(已过时,仅支持无参构造)
User user1 = (User) cls.newInstance();

// 方式2:使用Constructor的newInstance()(推荐,支持任意构造)
// 步骤1:获取指定参数类型的构造方法
Constructor<?> constructor = cls.getConstructor(String.class, int.class); // 假设User有(String, int)构造
// 步骤2:调用构造方法创建对象
User user2 = (User) constructor.newInstance("张三", 20); // 传入构造参数

3. 通过反射调用方法

通过Method类可以动态调用对象的方法,包括公有方法和私有方法:

java

运行

Class<?> cls = User.class;
User user = (User) cls.newInstance();

// 步骤1:获取方法(参数:方法名 + 方法参数类型)
Method setNameMethod = cls.getMethod("setName", String.class); // 获取public方法
// 若要获取私有方法,用getDeclaredMethod()
Method privateMethod = cls.getDeclaredMethod("privateMethod", int.class);

// 步骤2:调用方法(参数:对象实例 + 方法参数)
setNameMethod.invoke(user, "李四"); // 调用public方法

// 调用私有方法需先设置setAccessible(true)
privateMethod.setAccessible(true); // 关闭访问检查(关键)
privateMethod.invoke(user, 100); // 调用私有方法

4. 通过反射访问 / 修改成员变量

通过Field类可以动态操作对象的属性,包括私有属性:

java

运行

Class<?> cls = User.class;
User user = (User) cls.newInstance();

// 步骤1:获取字段(公有字段用getField(),私有字段用getDeclaredField())
Field ageField = cls.getDeclaredField("age"); // 假设age是私有字段

// 步骤2:访问/修改字段(私有字段需先设置setAccessible(true))
ageField.setAccessible(true); // 关闭访问检查
ageField.set(user, 25); // 设置值
int age = (int) ageField.get(user); // 获取值
System.out.println(age); // 输出:25

四、反射的应用场景

反射的动态性使其在以下场景中不可或缺:

  1. 框架开发
    Spring 的 IOC 容器通过反射创建对象并注入依赖;MyBatis 通过反射将数据库结果集映射为 Java 对象。
  2. 动态代理
    实现 AOP(面向切面编程)的基础,如 Spring AOP 通过反射在方法执行前后插入增强逻辑。
  3. 注解处理
    注解本身不具备功能,需通过反射解析注解并执行逻辑(如 JUnit 的@Test、Spring 的@Autowired)。
  4. 序列化 / 反序列化
    JSON 框架(如 Jackson、Gson)通过反射将 JSON 字符串转换为 Java 对象。
  5. 工具类开发
    如 BeanUtils(属性拷贝)、日志框架等,需动态访问对象的属性和方法。

五、反射的优缺点

优点:

  • 灵活性高:可在运行时动态操作类,无需编译期确定具体类。
  • 解耦:降低代码间的依赖,便于框架设计(如通过配置文件动态加载类)。

缺点:

  • 性能损耗:反射操作需要解析字节码,绕过编译期检查,性能比直接调用低(约为直接调用的 1/10 到 1/30)。
  • 破坏封装:可通过setAccessible(true)访问私有成员,违反类的封装设计。
  • 代码可读性差:反射代码较为繁琐,且动态操作使逻辑更难追踪。

六、反射性能优化

在性能敏感场景中,可通过以下方式优化反射:

  1. 缓存反射对象
    缓存ClassMethodField对象,避免频繁获取(反射的性能损耗主要在获取对象阶段)。

    java

    运行

    // 示例:缓存Method对象
    private static final Map<String, Method> methodCache = new HashMap<>();
    
    public static Method getCachedMethod(Class<?> cls, String methodName, Class<?>... paramTypes) throws NoSuchMethodException {
        String key = cls.getName() + "#" + methodName + Arrays.toString(paramTypes);
        if (!methodCache.containsKey(key)) {
            methodCache.put(key, cls.getMethod(methodName, paramTypes));
        }
        return methodCache.get(key);
    }
    
  2. 使用setAccessible(true)
    关闭访问检查可减少安全校验的开销(尤其对私有成员操作)。

  3. 替代方案
    在性能敏感场景,优先使用直接调用;JDK 9 + 引入的MethodHandles性能优于传统反射,可作为替代。

七、关于反射的常见面试题

1. 什么是 Java 反射?

反射是 Java 编程语言的一种特性,允许程序在运行时获取类的信息(如类名、方法、字段等),并能动态操作类或对象的属性和方法,而无需在编译期知道具体的类信息。

2. 反射的主要用途有哪些?

  • 框架开发(如 Spring 的 IOC 容器通过反射创建对象)
  • 动态代理实现(AOP 的基础)
  • 序列化与反序列化
  • 注解处理器实现
  • 动态调用类的方法或访问属性

3. 反射的核心类有哪些?

  • Class:表示类的字节码对象,是反射的入口
  • Constructor:类的构造方法
  • Method:类的方法
  • Field:类的成员变量
  • Modifier:用于解析成员的修饰符

4. 如何获取 Class 对象?

有三种常用方式:

java

运行

// 1. 通过类名.class
Class<?> cls1 = User.class;

// 2. 通过对象.getClass()
User user = new User();
Class<?> cls2 = user.getClass();

// 3. 通过Class.forName("全类名")
Class<?> cls3 = Class.forName("com.example.User");

5. 反射的优缺点是什么?

优点

  • 提高程序灵活性和扩展性

  • 可实现动态创建和操作对象

  • 是许多框架的底层基础

缺点

  • 性能开销较大(反射操作绕过编译期检查)
  • 破坏封装性(可访问私有成员)
  • 代码可读性降低,调试困难

6. 如何通过反射创建对象?

两种方式:

java

运行

// 1. 使用Class的newInstance()(已过时,推荐用Constructor)
Class<?> cls = User.class;
User user1 = (User) cls.newInstance();

// 2. 使用Constructor的newInstance()
Constructor<?> constructor = cls.getConstructor(String.class, int.class);
User user2 = (User) constructor.newInstance("张三", 20);

7. 如何通过反射调用方法?

java

运行

Class<?> cls = User.class;
Object obj = cls.newInstance();

// 获取方法(参数:方法名,参数类型)
Method method = cls.getMethod("setName", String.class);

// 调用方法(参数:对象实例,方法参数)
method.invoke(obj, "李四");

8. 如何通过反射访问私有成员?

需要设置setAccessible(true)绕过访问检查:

java

运行

Class<?> cls = User.class;
Object obj = cls.newInstance();

// 获取私有字段
Field field = cls.getDeclaredField("age");
field.setAccessible(true); // 关键:允许访问私有成员
field.set(obj, 25); // 设置值
int age = (int) field.get(obj); // 获取值

9. 反射为什么会影响性能?

  • 反射操作需要解析字节码,生成额外的对象
  • 绕过了编译期类型检查,所有操作在运行时进行
  • JVM 难以对反射操作进行优化

10. 如何优化反射性能?

  • 缓存反射对象(如 Class、Method、Field)
  • 尽量使用非反射方式替代(在性能敏感场景)
  • 使用setAccessible(true)减少安全检查开销
  • JDK 9 + 引入的MethodHandles可提供更好性能

11. 反射是否可以访问被private修饰的成员?为什么?

可以。通过getDeclaredXXX()方法获取私有成员,并调用setAccessible(true)关闭访问检查即可访问。这是因为 Java 的访问控制是编译期的限制,运行时可以通过反射突破。

12. 反射和注解有什么关系?

反射是处理注解的基础。通过反射可以:

  • 获取类 / 方法 / 字段上的注解
  • 判断是否存在特定注解
  • 根据注解信息执行相应逻辑(如 Spring 的@Autowired依赖注入)