Skip to content

反射与注解 ​

学习目标 ​

  • 能通过 Class、构造器、方法和字段读取受控类型的元数据。
  • 能定义并读取 RUNTIME 注解,分清注解声明、使用和解析。
  • 能判断什么时候该用正常多态,什么时候反射或动态代理才有价值。

核心知识点 ​

专业术语 ​

  • Reflection:运行时检查类型结构,并按名称或元数据动态调用成员。
  • Class:代表运行时类型信息的对象,是大多数反射操作的入口。
  • Annotation:附着在代码元素上的结构化元数据,由元注解描述范围和保留策略。
  • Encapsulation:类通过访问控制保护实现细节;反射不能成为随意绕过封装的理由。

白话解释与边界 ​

反射适合框架注册、序列化、测试工具和插件加载,因为调用方直到运行时才知道具体类型。普通业务代码知道目标类型时,直接调用接口更安全、更易重构,也更容易被编译器检查。RUNTIME 注解才能被运行时读取;强模块边界下,opens 和访问权限还会影响反射。需要接口拦截时再阅读 动态代理与反射边界,不要把所有动态需求都塞进字段或方法反射。

常用用法 ​

Class:获取运行时类型 ​

三种常见入口是类字面量、对象 getClass() 和 Class.forName();最后一种会按名称加载类,可能触发初始化并抛出检查异常。

java
// 说明:String.class 不依赖实例,"java".getClass() 从对象取类型;literal 与 objectType 最终引用同一个 Class<String> 对象。
public class ClassSourceDemo {
    public static void main(String[] args) {
        Class<String> literal = String.class;
// 初始状态:literal = String.class。
// 作用:Class<String> literal = String.class;;获取运行时类型。
        Class<?> objectType = "java".getClass();
// 初始状态:objectType = "java".getClass()。
        System.out.println(literal == objectType);
// 输出:true
    }
}

完整调用 API 见 Class 与反射常用 API。

定义注解边界:约束保留期与使用位置 ​

RetentionPolicy.RUNTIME 决定运行时是否保留,Target 决定可以标在什么元素上;定义注解时先限制范围,避免把任意字符串约定散落在业务代码中。

java
// 说明:定义注解边界:约束保留期与使用位置。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Timed { }

public class AnnotationDeclarationDemo {
    @Timed
    static void run() { }

    public static void main(String[] args) {
        System.out.println(runMethod().isAnnotationPresent(Timed.class));
// 输出:true
    }

    private static java.lang.reflect.Method runMethod() {
        try {
            return AnnotationDeclarationDemo.class.getDeclaredMethod("run");
// 初始状态:return AnnotationDeclarationDemo.class.getDeclaredMethod("run")。
// 作用:return AnnotationDeclarationDemo.class.getDeclaredMethod("run");;约束保留期与使用位置。
        } catch (NoSuchMethodException e) {
            throw new AssertionError(e);
// 初始状态:throw new AssertionError(e)。
// 作用:throw new AssertionError(e);;约束保留期与使用位置。
        }
    }
}

注解的完整读取见 注解定义与运行时读取。

getAnnotation:读取运行时注解 ​

读取注解时要区分“没有注解”和“注解存在但值为空”;如果依赖继承行为,还要明确 @Inherited 只对类级注解生效,不会自动覆盖方法和字段。

java
// 说明:getAnnotation:读取运行时注解。
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
@interface Route { String value(); }

@Route("/users")
class UserController { }

public class AnnotationReadDemo {
    public static void main(String[] args) {
        Route route = UserController.class.getAnnotation(Route.class);
// 初始状态:route = UserController.class.getAnnotation(Route.class)。
// 作用:Route route = UserController.class.getAnnotation(Route.class);;读取运行时注解。
        System.out.println(route.value());
// 输出:/users
    }
}

反射与正常多态的选择 ​

已知接口时直接多态优先;只有实现类由配置、插件或框架在运行时决定时,才把反射集中在边界层,并校验构造器、类型和异常。

java
// 说明:反射与正常多态的选择。
interface Greeter { String greet(String name); }

class ChineseGreeter implements Greeter {
    public String greet(String name) { return "你好," + name; }
}

public class ReflectionBoundaryDemo {
    public static void main(String[] args) {
        Greeter greeter = new ChineseGreeter();
// 初始状态:greeter = new ChineseGreeter()。
// 作用:Greeter greeter = new ChineseGreeter();;greeter 引用 ChineseGreeter,greet("Java") 输出 你好,Java。
        System.out.println(greeter.greet("Java"));
// 输出:你好,Java
    }
}

不常用但需要知道 ​

setAccessible 与模块封装 ​

trySetAccessible() 会报告当前访问是否能被打开,强行 setAccessible(true) 不是跨模块的通行证;在模块化应用中应通过 opens 精确开放反射包,并尽量减少私有成员访问。

java
// 说明:getDeclaredField("value") 取得 Secret 的私有字段;trySetAccessible() 只在当前未命名模块边界允许时返回 true。
import java.lang.reflect.Field;

class Secret { private String value = "hidden"; }
// 初始状态:value = "hidden"; }。

public class AccessibleBoundaryDemo {
    public static void main(String[] args) throws Exception {
        Field field = Secret.class.getDeclaredField("value");
// 初始状态:field = Secret.class.getDeclaredField("value")。
// 作用:Field field = Secret.class.getDeclaredField("value");;按字段名 "value" 获取 Field,trySetAccessible() 输出 true。
        System.out.println(field.trySetAccessible());
// 输出:true
    }
}

MethodHandles:更明确的动态调用入口 ​

MethodHandle 仍受查找上下文和访问权限约束,但类型信息更明确;不要为了“看起来更快”就把简单方法调用全部改成句柄。

java
// 作用:通过 MethodHandles 更明确的动态调用入口。
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

public class MethodHandleDemo {
    static String upper(String value) { return value.toUpperCase(); }
// 作用:static String upper(String value) { return value.toUpperCase(); };更明确的动态调用入口。

    public static void main(String[] args) throws Throwable {
        var handle = MethodHandles.lookup().findStatic(
                MethodHandleDemo.class, "upper", MethodType.methodType(String.class, String.class));
// 初始状态:handle = MethodHandles.lookup().findStatic(。
// 作用:var handle = MethodHandles.lookup().findStatic(;更明确的动态调用入口。
        System.out.println((String) handle.invokeExact("java"));
// 输出:JAVA
    }
}

getRecordComponents:读取 record 结构 ​

record 的组件可以通过 RecordComponent 读取,但这只是元数据访问,不会把 record 变成可修改的 JavaBean;框架仍需处理构造器、可空值和版本兼容。

java
// 作用:通过 getRecordComponents 读取 record 结构。
record User(String name, int age) { }

public class RecordReflectionDemo {
    public static void main(String[] args) {
        System.out.println(User.class.getRecordComponents()[0].getName());
// 输出:name
    }
}

泛型类型信息与擦除 ​

泛型大多在运行时擦除,只有类签名、字段声明或方法签名中的部分元数据仍可读取;不要假定 List<String> 在所有运行时对象上都能直接得到 String。

java
import java.lang.reflect.ParameterizedType;
import java.util.List;

class Names extends java.util.ArrayList<String> { }

public class GenericTypeDemo {
    public static void main(String[] args) {
        var type = Names.class.getGenericSuperclass();
// 初始状态:type = Names.class.getGenericSuperclass()。
// 作用:var type = Names.class.getGenericSuperclass();;读取 Names 的泛型父类,argument 最终为 java.lang.String。
        var argument = ((ParameterizedType) type).getActualTypeArguments()[0];
// 初始状态:argument = ((ParameterizedType) type).getActualTypeArguments()[0]。
        System.out.println(argument.getTypeName());
// 输出:java.lang.String
    }
}

简单案例 ​

java
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

class Greeting {
    private final String prefix;

    Greeting(String prefix) { this.prefix = prefix; }
// 初始状态:prefix = prefix; }。

    public String say(String name) { return prefix + name; }
}

public class ReflectionDemo {
    public static void main(String[] args) throws Exception {
        Constructor<Greeting> constructor = Greeting.class.getDeclaredConstructor(String.class);
// 初始状态:constructor = Greeting.class.getDeclaredConstructor(String.class)。
// 作用:Constructor<Greeting> constructor = Greeting.class.getDeclaredConstructor(String.class);;按 String.class 获取 Greeting 构造器。
        Greeting greeting = constructor.newInstance("Hello, ");
// 初始状态:greeting = constructor.newInstance("Hello, ")。
// 作用:Greeting greeting = constructor.newInstance("Hello, ");;用前缀 "Hello, " 创建 Greeting。
        Method method = Greeting.class.getMethod("say", String.class);
// 初始状态:method = Greeting.class.getMethod("say", String.class)。
// 作用:Method method = Greeting.class.getMethod("say", String.class);;按名称 "say" 和 String.class 获取方法,后续传入 "Java"。
        System.out.println(method.invoke(greeting, "Java"));
// 输出:Hello, Java
    }
}

示例把反射限制在已知类型边界;真实框架还需要缓存成员、校验参数、解包 InvocationTargetException,并保留目标异常的根因。

易混点 ​

  • getMethods() 只返回公共方法(含继承成员),getDeclaredMethods() 返回当前类声明的方法(含非 public)。
  • Class.forName() 是按名称加载,类字面量不会触发同样的初始化路径;两者不能随意互换。
  • RUNTIME 是“可在运行时读取”,不是“可以修改注解”;注解实例是元数据视图。
  • setAccessible 不是绕过模块系统的万能开关,封装和 opens 仍然有效。

课后小问 ​

  1. 为什么只写 @Retention(SOURCE) 的注解在运行时读不到? 答案:SOURCE 注解只在源代码处理阶段存在,编译产物和运行时都不保留它。 解析:需要反射读取时使用 RUNTIME;只供编译器或注解处理器使用时才选 SOURCE。

  2. 已知实现类时为什么不建议用反射替代接口调用? 答案:接口调用有编译期类型检查和重构支持,反射把错误推迟到运行时。 解析:反射应集中在插件、框架等动态边界,业务内部继续使用正常多态更清晰。

本节小结 ​

  • Class 是运行时类型入口,构造器、方法和字段构成反射调用的主要对象。
  • 注解的 Retention、Target 和继承策略决定了它能否、在哪里被读取。
  • 反射适合动态边界,不应取代已知类型的接口和多态。
  • 模块封装、访问权限、异常包装和泛型擦除是反射的主要边界。

快速回顾 ​

  • 类字面量、getClass、Class.forName 是三种常见类型入口。
  • RUNTIME 注解才能用 getAnnotation 读取。
  • getDeclared* 看当前类声明,get* 通常包含公共继承成员。
  • 反射代码要缓存、校验参数,并保留被调用方法的真实异常。