反射与注解
学习目标
- 能通过
Class、构造器、方法和字段读取受控类型的元数据。 - 能定义并读取
RUNTIME注解,分清注解声明、使用和解析。 - 能判断什么时候该用正常多态,什么时候反射或动态代理才有价值。
核心知识点
专业术语
- Reflection:运行时检查类型结构,并按名称或元数据动态调用成员。
- Class:代表运行时类型信息的对象,是大多数反射操作的入口。
- Annotation:附着在代码元素上的结构化元数据,由元注解描述范围和保留策略。
- Encapsulation:类通过访问控制保护实现细节;反射不能成为随意绕过封装的理由。
白话解释与边界
反射适合框架注册、序列化、测试工具和插件加载,因为调用方直到运行时才知道具体类型。普通业务代码知道目标类型时,直接调用接口更安全、更易重构,也更容易被编译器检查。RUNTIME 注解才能被运行时读取;强模块边界下,opens 和访问权限还会影响反射。需要接口拦截时再阅读 动态代理与反射边界,不要把所有动态需求都塞进字段或方法反射。
常用用法
Class:获取运行时类型
三种常见入口是类字面量、对象 getClass() 和 Class.forName();最后一种会按名称加载类,可能触发初始化并抛出检查异常。
// 说明: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 决定可以标在什么元素上;定义注解时先限制范围,避免把任意字符串约定散落在业务代码中。
// 说明:定义注解边界:约束保留期与使用位置。
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 只对类级注解生效,不会自动覆盖方法和字段。
// 说明: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
}
}反射与正常多态的选择
已知接口时直接多态优先;只有实现类由配置、插件或框架在运行时决定时,才把反射集中在边界层,并校验构造器、类型和异常。
// 说明:反射与正常多态的选择。
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 精确开放反射包,并尽量减少私有成员访问。
// 说明: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 仍受查找上下文和访问权限约束,但类型信息更明确;不要为了“看起来更快”就把简单方法调用全部改成句柄。
// 作用:通过 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;框架仍需处理构造器、可空值和版本兼容。
// 作用:通过 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。
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
}
}简单案例
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仍然有效。
课后小问
为什么只写
@Retention(SOURCE)的注解在运行时读不到? 答案:SOURCE 注解只在源代码处理阶段存在,编译产物和运行时都不保留它。 解析:需要反射读取时使用 RUNTIME;只供编译器或注解处理器使用时才选 SOURCE。已知实现类时为什么不建议用反射替代接口调用? 答案:接口调用有编译期类型检查和重构支持,反射把错误推迟到运行时。 解析:反射应集中在插件、框架等动态边界,业务内部继续使用正常多态更清晰。
本节小结
- Class 是运行时类型入口,构造器、方法和字段构成反射调用的主要对象。
- 注解的 Retention、Target 和继承策略决定了它能否、在哪里被读取。
- 反射适合动态边界,不应取代已知类型的接口和多态。
- 模块封装、访问权限、异常包装和泛型擦除是反射的主要边界。
快速回顾
- 类字面量、
getClass、Class.forName是三种常见类型入口。 RUNTIME注解才能用getAnnotation读取。getDeclared*看当前类声明,get*通常包含公共继承成员。- 反射代码要缓存、校验参数,并保留被调用方法的真实异常。
阿源的知识库