Skip to content

反射优缺点

哲学课上,老师问:什么是自由?

有人说「想做什么就做什么」,老师说「那叫任性」。

有人说「知道边界在哪里,还能做出选择」,老师说「接近了」。

反射就是这样——它给了你「想做什么就做什么」的能力,但真正的智慧在于知道什么时候用它,什么时候不用。

反射的优势

1. 动态性:推迟决策到运行时

正常写代码,类名、方法名都是编译期确定的:

java
// 硬编码——编译后就定型了
UserService service = new UserServiceImpl();
service.save(user);

用反射,类名可以从配置文件读取:

java
// 软编码——运行时才决定
String className = config.get("service.class");  // "com.example.UserServiceImpl"
String methodName = config.get("service.method"); // "save"

Class<?> clazz = Class.forName(className);
Object service = clazz.getDeclaredConstructor().newInstance();
Method method = clazz.getDeclaredMethod(methodName, User.class);
method.invoke(service, user);

这意味着什么?换实现不用改代码,改配置就行

插件系统、热加载、A/B 测试——这些能力全都依赖反射的动态性。

2. 通用性:一套代码处理所有类

不用反射,每个类都要单独处理:

java
// 不用反射——每种类型都要硬编码
if (obj instanceof User) {
    User u = (User) obj;
    handleUser(u);
} else if (obj instanceof Order) {
    Order o = (Order) obj;
    handleOrder(o);
} else if (obj instanceof Product) {
    Product p = (Product) obj;
    handleProduct(p);
}
// ... 无穷无尽

用反射,一套代码处理所有类型:

java
// 用反射——类型无关的通用逻辑
public void serialize(Object obj) {
    for (Field field : obj.getClass().getDeclaredFields()) {
        field.setAccessible(true);
        System.out.println(field.getName() + " = " + field.get(obj));
    }
}
// User、Order、Product 共用同一套代码

ORM 框架、JSON 序列化器、通用调试工具——全都靠这套模式。

3. 框架基石:没有反射就没有 Spring

Spring 的依赖注入是怎么实现的?

java
public class SpringDIExample {

    // 你只需要声明
    @Autowired
    private UserRepository userRepository;

    // Spring 在背后做这些:
    public void inject(Object bean) throws Exception {
        for (Field field : bean.getClass().getDeclaredFields()) {
            if (field.isAnnotationPresent(Autowired.class)) {
                // 找类型匹配的 Bean
                Object dependency = applicationContext.getBean(field.getType());
                // 注入进去
                field.setAccessible(true);
                field.set(bean, dependency);
            }
        }
    }
}

没有反射,这段代码根本无法实现——Java 编译器根本不知道「把 UserRepository 塞给 userRepository 字段」这件事。

反射的劣势

1. 性能开销:比直接调用慢 10-50 倍

这是有代价的。反射调用的开销主要来自:

  • 安全检查:每次调用都要检查访问权限
  • 类型检查:参数类型需要运行时验证
  • 方法分派:无法被 JIT 优化内联
java
public class PerformanceComparison {

    public int add(int a, int b) { return a + b; }

    public static void main(String[] args) throws Exception {
        PerformanceComparison obj = new PerformanceComparison();

        // 直接调用:~1ns
        long start = System.nanoTime();
        for (int i = 0; i &lt; 1_000_000; i++) {
            obj.add(1, 2);
        }
        long direct = System.nanoTime() - start;

        // 反射调用:~50-100ns
        Method method = PerformanceComparison.class.getDeclaredMethod("add", int.class, int.class);
        start = System.nanoTime();
        for (int i = 0; i &lt; 1_000_000; i++) {
            method.invoke(obj, 1, 2);
        }
        long reflect = System.nanoTime() - start;

        System.out.println("直接调用: " + direct / 1_000_000 + "ms");
        System.out.println("反射调用: " + reflect / 1_000_000 + "ms");
        System.out.println("性能差距: ~" + (reflect / direct) + "x");
    }
}

重要:这不是让你不用反射,而是让你知道在哪里用它。Spring 启动时用反射创建 Bean 用个几百次,没人在意。但如果在一个循环里每毫秒调用几千次,性能就堪忧了。

2. 安全风险:潘多拉魔盒

反射可以绕过 Java 的访问控制修饰符。这意味着:

java
public class SecurityRiskDemo {

    static class SecureData {
        private String password = "super_secret_123";
    }

    public static void main(String[] args) throws Exception {
        SecureData data = new SecureData();

        // 正常情况下,password 不可访问
        // 但反射可以...

        Field passwordField = SecureData.class.getDeclaredField("password");
        passwordField.setAccessible(true);

        // 密码泄露!
        String password = (String) passwordField.get(data);
        System.out.println("密码是: " + password);  // 不要在生产环境这样玩
    }
}

恶意代码可以利用反射:

  • 读取 private 字段(密码、密钥)
  • 修改 final 字段(绕过不可变性)
  • 调用 private 方法(绕过业务逻辑检查)

这就是为什么安全管理器(SecurityManager)和模块系统(JDK 9+)要限制反射的使用。

3. 代码可读性差:维护者的噩梦

java
// 这段代码是什么意思?
Object result = clazz.getDeclaredMethod(
    config.get("method"),
    Stream.of(config.get("paramTypes").split(","))
          .map(this::loadClass)
          .toArray(Class[]::new)
).invoke(
    clazz.getDeclaredConstructor().newInstance(),
    args
);

// 对比同样逻辑的正常代码:
UserService service = new UserServiceImpl();
service.processOrder(order);

反射代码往往难以理解、难以调试、难以维护。你写的反射代码,三天后自己都看不懂。

4. 编译期检查失效:bug 延迟到运行时

java
// 正常代码——编译期就检查
User user = new User();
user.setAge("25");  // 编译错误!类型不匹配

// 反射代码——运行才知道错
Method setter = clazz.getDeclaredMethod("setAge", String.class);
setter.invoke(user, 25);  // 编译通过,运行才报错

类型安全、null 安全、方法存在性——这些在编译期能发现的问题,反射都推迟到了运行时。

使用场景分析

适合用反射的场景

场景为什么适合示例
框架开发需要通用性、一致性Spring DI、MyBatis、Hibernate
工具开发需要操作未知类型JSON 序列化、通用调试工具
测试框架需要访问内部实现JUnit、Mockito
插件系统需要运行时加载IDE 插件系统
配置驱动需要灵活切换实现数据库驱动加载

不适合用反射的场景

场景为什么不适合替代方案
频繁调用的业务代码性能开销累积直接调用、策略模式
对安全性要求高的代码风险难以控制明确接口、分层设计
简单明确的场景过度设计直接 new、正常调用

最佳实践

1. 缓存反射结果

获取 Method/Field/Constructor 是有开销的,重复获取就是浪费:

java
// 不好:每次调用都获取
public void process(Object obj) throws Exception {
    Method method = obj.getClass().getDeclaredMethod("doWork");
    method.invoke(obj);
}

// 好:缓存起来
private final ConcurrentHashMap<Class&lt;?&gt;, Method&gt; methodCache = new ConcurrentHashMap&lt;&gt;();

public void process(Object obj) throws Exception {
    Method method = methodCache.computeIfAbsent(
        obj.getClass(),
        c -&gt; {
            try {
                return c.getDeclaredMethod("doWork");
            } catch (NoSuchMethodException e) {
                throw new RuntimeException(e);
            }
        }
    );
    method.invoke(obj);
}

2. 异常处理要分层

反射会抛出多种异常,处理好它们:

java
public Object invokeSafely(Object target, String methodName, Object... args) {
    try {
        Method method = target.getClass().getDeclaredMethod(methodName);
        method.setAccessible(true);
        return method.invoke(target, args);
    } catch (NoSuchMethodException e) {
        throw new IllegalArgumentException("方法不存在: " + methodName, e);
    } catch (IllegalAccessException e) {
        throw new SecurityException("无权访问方法: " + methodName, e);
    } catch (InvocationTargetException e) {
        // 方法内部的异常,unwrap 出来
        throw new RuntimeException("方法执行失败", e.getCause());
    }
}

3. 考虑用 MethodHandle 代替反射(JDK 7+)

MethodHandle 性能更好,但 API 更复杂:

java
import java.lang.invoke.*;

public class MethodHandleVsReflection {

    public String greet(String name) { return "Hello, " + name; }

    public static void main(String[] args) throws Throwable {
        Class&lt;?&gt; clazz = MethodHandleVsReflection.class;
        MethodHandleDemo obj = new MethodHandleDemo();

        // MethodHandle:性能更好,但编译期检查更少
        MethodHandle handle = MethodHandles.lookup()
            .findVirtual(clazz, "greet", MethodType.methodType(String.class, String.class));

        String result = (String) handle.invoke(obj, "World");
        System.out.println(result);
    }
}

要点回顾

维度优势劣势
动态性运行时决定类型和方法编译期检查失效
通用性一套代码处理所有类代码可读性差
框架框架的基石性能开销大
安全绕过访问控制

核心原则:反射是工具,不是银弹。在需要动态性、通用性的地方使用它;在需要性能、安全性的地方绕过它。理解它的代价,才能用好它。

基于 VitePress 构建