反射原理与应用场景

面试官问:"Java 反射是什么?用过吗?"

候选人小郑答:"反射是在运行时动态获取类的信息,比如类名、方法、字段等。"

面试官追问:"那 Class 对象是什么时候创建的?类加载的时候还是第一次使用时?"

小郑说:"应该是类加载的时候?"

面试官写了一段代码:

class Dog { }

Class<?> c1 = Dog.class;    // 这时候 Class 对象创建了吗?
Class<?> c2 = Class.forName("com.example.Dog"); // 这时候呢?
Dog d = new Dog();
Class<?> c3 = d.getClass(); // 这时候呢?

小郑答不上来。

【面试官心理】 这道题考的是候选人对类加载时机和 Class 对象创建的理解。能说出"加载-链接-初始化"流程的候选人,是真正看过 JVM 规范的。

一、反射的核心:Class 对象 🔴

1.1 三种获取 Class 对象的方式

// 方式一:类字面量(编译期确定,不会触发类初始化)
Class<?> c1 = Dog.class;

// 方式二:Class.forName()(会触发类初始化)
Class<?> c2 = Class.forName("com.example.Dog");

// 方式三:对象.getClass()(运行时)
Dog d = new Dog();
Class<?> c3 = d.getClass();

// 三种方式获取的是同一个 Class 对象
c1 == c2 == c3; // true
⚠️

Class.forName() 默认会触发类初始化(执行 static 块),而 .class 不会。在 JDBC 驱动注册中必须用 Class.forName("com.mysql.jdbc.Driver") 正是利用了这个特性来触发驱动的 static 初始化块。

1.2 Class 对象的创建时机

Class 对象是类加载过程中创建的:

graph LR
    A["加载 Loading"] --> B["链接 Linking<br/>验证/准备/解析"]
    B --> C["初始化 Initialization<br/>执行 static 块"]
    C --> D["Class 对象创建"]

类加载(Loading)阶段:ClassLoader 读取 .class 文件,生成 Class 对象,放入方法区。

二、反射的核心 API 🔴

2.1 获取类信息

Class<?> cls = Class.forName("java.util.ArrayList");

// 获取所有构造器
Constructor<?>[] cons = cls.getConstructors();

// 获取所有方法
Method[] methods = cls.getMethods(); // 包括继承的 public 方法

// 获取 declared 方法(包括 private)
Method[] declared = cls.getDeclaredMethods();

// 获取所有字段
Field[] fields = cls.getDeclaredFields();

2.2 动态创建对象

// 通过无参构造器创建
Object obj = cls.getDeclaredConstructor().newInstance();

// 通过指定构造器创建
Constructor<?> constructor = cls.getConstructor(String.class, int.class);
Object obj = constructor.newInstance("hello", 42);

2.3 动态调用方法

Method method = cls.getMethod("add", Object.class);
method.invoke(obj, "value"); // 相当于 obj.add("value")

// 调用 private 方法
Method privateMethod = cls.getDeclaredMethod("secretMethod");
privateMethod.setAccessible(true); // 绕过访问检查
privateMethod.invoke(obj);

2.4 动态访问字段

Field field = cls.getDeclaredField("name");
field.setAccessible(true); // 绕过访问检查
field.set(obj, "newName"); // 相当于 obj.name = "newName"

Object value = field.get(obj); // 相当于 obj.name

三、应用场景 🔴

3.1 Spring 框架的反射应用

// Spring IOC:根据配置创建 Bean
// <bean id="userService" class="com.example.UserService"/>

// 内部实现(简化)
public Object createBean(String className) {
    Class<?> cls = Class.forName(className);
    return cls.getDeclaredConstructor().newInstance();
}

// Spring 通过反射:
// 1. 读取 XML/注解配置中的类名
// 2. 用 Class.forName() 加载类
// 3. 用 newInstance() 创建实例
// 4. 用反射遍历字段,注入 @Autowired 依赖

3.2 Hibernate/MyBatis 的反射应用

// MyBatis:结果集自动映射到对象
// <select id="findUser" resultType="User">
//   SELECT id, name FROM users WHERE id = #{id}
// </select>

// 内部实现(简化)
public <T> T mapRow(ResultSet rs, Class<T> entityClass) {
    T entity = entityClass.getDeclaredConstructor().newInstance();
    for (Field field : entityClass.getDeclaredFields()) {
        field.setAccessible(true);
        field.set(entity, rs.getObject(toColumnName(field.getName()), field.getType()));
    }
    return entity;
}

3.3 Jackson/Gson 的反射应用

// Jackson:JSON 自动序列化/反序列化
User user = new ObjectMapper().readValue(jsonString, User.class);

// 内部实现(简化)
public <T> T fromJson(String json, Class<T> cls) {
    T obj = cls.getDeclaredConstructor().newInstance();
    JsonObject jo = JsonParser.parseString(json).getAsJsonObject();
    for (Field field : cls.getDeclaredFields()) {
        field.setAccessible(true);
        Object value = convert(jo.get(field.getName()), field.getType());
        field.set(obj, value);
    }
    return obj;
}

3.4 通用对象拷贝工具

public class BeanUtils {
    public static void copyProperties(Object source, Object target) {
        Class<?> cls = source.getClass();
        for (Field field : cls.getDeclaredFields()) {
            field.setAccessible(true);
            Field targetField = target.getClass().getDeclaredField(field.getName());
            targetField.setAccessible(true);
            targetField.set(target, field.get(source));
        }
    }
}

四、反射的性能代价 🔴

4.1 性能问题来源

// 反射调用的性能开销:
// 1. setAccessible(true) 需要遍历调用栈检查安全管理器
// 2. invoke() 每次都需要参数数组包装
// 3. 泛型擦除导致类型转换
// 4. JIT 无法内联反射方法

// 性能对比(相对值)
正常方法调用:   1x
MethodHandle:   2-3x
反射调用:       20-50x
setAccessible:  额外 +5x

4.2 优化策略

// 策略一:缓存 Method/Field 对象(只反射一次)
private static final Map<String, Method> METHOD_CACHE = new ConcurrentHashMap<>();

public static Object invoke(Object target, String methodName, Object... args) {
    Class<?> cls = target.getClass();
    String key = cls.getName() + methodName;
    Method method = METHOD_CACHE.computeIfAbsent(key, k -> {
        // 反射获取一次
        return cls.getDeclaredMethod(methodName);
    });
    return method.invoke(target, args);
}

// 策略二:使用 MethodHandle(JDK 7+,性能好于反射)
MethodHandle mh = MethodHandles.lookup()
    .findVirtual(cls, "methodName", MethodType.methodType(returnType, paramTypes))
    .bindTo(target);
return mh.invokeWithArguments(args);
💡

Spring 3.0+、MyBatis、Hibernate 等框架都使用了反射缓存优化。能说出"缓存反射对象"作为优化策略的候选人,面试官会认为有框架源码阅读经验。

五、安全问题(模块化)

JDK 9 模块系统引入了更严格的反射限制:

// JDK 9+ 中,某些模块的 private 成员即使 setAccessible(true) 也无法访问
Method m = cls.getDeclaredMethod("secret");
m.setAccessible(true); // 在模块化环境下可能失败!

// 模块系统限制
// java.base 模块默认不允许 reflective access to private members
// 需要显式打开模块:--add-opens java.base/java.lang=ALL-UNNAMED

【面试官心理】 能说出 JDK 9 模块化对反射限制的候选人,说明关注了 Java 演进方向。这是 P6/P7 的加分点。

六、追问升级

面试官:"getDeclaredFields 和 getFields 有什么区别?"

class Parent {
    public String publicField;
    private String privateField;
}

class Child extends Parent {
    public String childPublic;
    private String childPrivate;
}

Child.class.getFields();        // 只获取 public 字段(包括继承的):childPublic, publicField
Child.class.getDeclaredFields(); // 获取本类声明的所有字段:childPublic, childPrivate

关键区别:getFields() 只返回 public 字段(包括继承的),getDeclaredFields() 返回本类声明的所有字段(包括 private)。