注解和反射
# 15.注解和反射
# 目录介绍
# 15.1 注解入门
# 15.1.1 注解是什么
注解(Annotation)是 Java 5 引入的一种元数据机制,用于为代码提供额外的信息。注解不会直接影响程序逻辑,但可以被编译器、框架或运行时工具读取和处理。
对比 C++:C++ 没有内置的注解机制(C++11 引入了 [[deprecated]] 等属性),Java 的注解系统更加完善和强大。注解是 Spring、MyBatis 等框架的核心基础。
注解的发展历程:
| 版本 | 新增内容 |
|---|---|
| JDK 5 | 引入注解机制:@Override、@Deprecated、@SuppressWarnings |
| JDK 6 | 引入注解处理器 API(APT,javax.annotation.processing) |
| JDK 7 | 支持 @SafeVarargs |
| JDK 8 | 类型注解(@Target(ElementType.TYPE_USE))、重复注解(@Repeatable) |
| JDK 9 | @Deprecated 增加 forRemoval 和 since 属性 |
| JDK 14 | @Serial 注解(序列化相关) |
JDK 8 重复注解的使用:
// JDK 8 之前,同一个位置不能重复使用同一个注解
// JDK 8+ 通过 @Repeatable 支持重复注解
@Repeatable(Schedules.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Schedule {
String dayOfWeek();
String time();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Schedules {
Schedule[] value();
}
// 使用重复注解
@Schedule(dayOfWeek = "Monday", time = "09:00")
@Schedule(dayOfWeek = "Wednesday", time = "14:00")
@Schedule(dayOfWeek = "Friday", time = "09:00")
public void weeklyMeeting() {
// 每周三次会议
}
// 通过反射读取重复注解
Method method = MyClass.class.getMethod("weeklyMeeting");
Schedule[] schedules = method.getAnnotationsByType(Schedule.class);
for (Schedule s : schedules) {
System.out.println(s.dayOfWeek() + " " + s.time());
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 15.1.2 内置注解
// @Override:标记方法重写了父类方法
@Override
public String toString() {
return "Account";
}
// @Deprecated:标记已过时的方法
@Deprecated
public void oldMethod() {
// 不推荐使用
}
// @SuppressWarnings:抑制编译器警告
@SuppressWarnings("unchecked")
public void method() {
List list = new ArrayList(); // 不会产生警告
}
// @FunctionalInterface:标记函数式接口(JDK 8+)
@FunctionalInterface
public interface MyFunction {
void execute();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 15.1.3 元注解
元注解是用于注解"注解"的注解:
| 元注解 | 作用 |
|---|---|
@Target | 注解可以用在哪里(类、方法、字段等) |
@Retention | 注解的生命周期(源码、编译、运行时) |
@Documented | 注解会包含在 javadoc 中 |
@Inherited | 注解可以被子类继承 |
@Target(ElementType.METHOD) // 只能用在方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时保留
public @interface MyAnnotation {
String value();
}
2
3
4
5
@Retention 的三种策略详解:
| 策略 | 生命周期 | 典型应用 |
|---|---|---|
SOURCE | 只存在于源码中,编译时丢弃 | @Override、@SuppressWarnings(编译器检查后不再需要) |
CLASS | 存在于 .class 文件中,但运行时不可见(默认值) | 某些字节码分析工具使用 |
RUNTIME | 运行时可通过反射获取 | Spring @Autowired、JUnit @Test 等框架注解 |
疑惑:为什么 @Override 只需要 SOURCE 级别而不需要 RUNTIME?
答疑:@Override 的唯一作用是让编译器检查"这个方法确实重写了父类方法"。一旦编译通过,这个检查就完成了,运行时不需要再读取这个注解。如果设为 RUNTIME,反而会增加 .class 文件大小和运行时开销。
论证:而 Spring 的 @Autowired 必须是 RUNTIME,因为 Spring 容器在运行时需要扫描类的字段,找到带 @Autowired 注解的字段,然后通过反射注入依赖。如果是 SOURCE 或 CLASS 级别,Spring 在运行时就读不到这个注解了。
结果展示:所以选择 @Retention 策略的原则很简单——如果注解只在编译期有用,用 SOURCE;如果运行时需要读取,用 RUNTIME。绝大多数自定义框架注解都应该选择 RUNTIME。
# 15.1.4 综合案例:内置注解使用指南
本案例综合运用 Java 内置注解和元注解,演示注解的常见使用场景。
import java.lang.annotation.*;
import java.util.ArrayList;
import java.util.List;
/**
* 内置注解使用指南 —— 注解入门综合案例
* 知识点:@Override、@Deprecated、@SuppressWarnings、@FunctionalInterface
*/
@FunctionalInterface // 标记函数式接口
interface Greeting {
String greet(String name);
}
class Animal {
public void speak() {
System.out.println("...");
}
@Deprecated(since = "2.0", forRemoval = true) // 标记过时
public void oldMethod() {
System.out.println("旧方法,请使用 newMethod()");
}
public void newMethod() {
System.out.println("新方法");
}
}
class Dog extends Animal {
@Override // 编译器检查是否正确重写
public void speak() {
System.out.println("汪汪!");
}
}
public class AnnotationGuide {
@SuppressWarnings("unchecked") // 抑制未检查警告
public static void main(String[] args) {
System.out.println("===== 内置注解使用指南 =====\n");
// 1. @Override
System.out.println("--- @Override ---");
Animal dog = new Dog();
dog.speak();
System.out.println(" 确保方法正确重写,拼写错误编译器会报错");
// 2. @Deprecated
System.out.println("\n--- @Deprecated ---");
dog.oldMethod(); // IDE 会显示删除线
dog.newMethod();
System.out.println(" 标记过时的API,编译时产生警告");
// 3. @SuppressWarnings
System.out.println("\n--- @SuppressWarnings ---");
List rawList = new ArrayList(); // 无泛型会有警告
rawList.add("hello");
System.out.println(" 抑制了 unchecked 警告");
// 4. @FunctionalInterface
System.out.println("\n--- @FunctionalInterface ---");
Greeting g = name -> "你好, " + name + "!";
System.out.println(" " + g.greet("Java"));
System.out.println(" 确保接口只有一个抽象方法,可用 Lambda");
// 5. 总结
System.out.println("\n--- 注解分类 ---");
System.out.println(" 标准注解 : @Override, @Deprecated, @SuppressWarnings");
System.out.println(" 元注解 : @Target, @Retention, @Documented, @Inherited");
System.out.println(" 自定义注解: @interface MyAnnotation {}");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# 15.1.5 注解入门训练题
训练1:写一个方法,故意不加 @Override 注解重写 toString() 方法,然后把方法名拼错为 tostring()。观察编译器是否报错。再加上 @Override 观察区别。
训练2:使用 @Deprecated 标记一个方法,然后在另一个方法中调用它。观察 IDE 和编译器的提示信息。
思考:Java 的注解本质上是一种特殊的接口(所有注解隐式继承 java.lang.annotation.Annotation)。这意味着什么?能否用 implements 来实现一个注解接口?
# 15.2 自定义注解
# 15.2.1 自定义注解语法
import java.lang.annotation.*;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiInfo {
String author() default "unknown";
String version() default "1.0";
String description();
}
2
3
4
5
6
7
8
9
# 15.2.2 自定义注解案例
@ApiInfo(author = "yangchong", version = "2.0", description = "银行账户类")
public class Account {
private String name;
private double balance;
@ApiInfo(description = "存款方法")
public void deposit(double amount) {
balance += amount;
}
}
2
3
4
5
6
7
8
9
10
# 15.2.3 注解的底层原理
注解在字节码中的表现:注解信息存储在 .class 文件的属性表中。对于 RUNTIME 注解,存储在 RuntimeVisibleAnnotations 属性中;对于 CLASS 注解,存储在 RuntimeInvisibleAnnotations 属性中。
注解的本质:每个注解在编译后会生成一个继承 java.lang.annotation.Annotation 的接口。当通过反射获取注解实例时(如 getAnnotation(ApiInfo.class)),JVM 会使用动态代理为这个接口创建一个代理对象,代理对象内部维护一个 Map,存储注解属性的名称和值。
// 反编译 @ApiInfo 的本质
public interface ApiInfo extends java.lang.annotation.Annotation {
String author(); // 这些是接口方法
String version();
String description();
}
// 当调用 cls.getAnnotation(ApiInfo.class) 时:
// JVM 返回一个 Proxy 对象,实现了 ApiInfo 接口
// 调用 info.author() 实际上是从内部 Map 中取值
2
3
4
5
6
7
8
9
10
# 15.2.4 自定义注解训练题
训练1:创建一个 @NotNull 注解(@Target(FIELD), @Retention(RUNTIME)),并编写一个验证方法,通过反射检查对象中所有标注了 @NotNull 的字段是否为 null。
训练2:创建一个 @Log 注解(@Target(METHOD), @Retention(RUNTIME)),包含 level 属性(默认 "INFO")。然后编写工具方法,扫描一个类中所有带 @Log 注解的方法并打印其名称和日志级别。
思考:注解属性只能是以下类型:基本类型、String、Class、枚举、注解、以上类型的数组。为什么不能是任意对象(比如 List、Map)?
# 15.3 反射机制
# 15.3.1 反射概念
反射(Reflection)是 Java 的一种机制,允许程序在运行时获取类的信息(类名、方法、字段等),并动态地创建对象、调用方法、访问字段。
反射的底层原理:每个类被 ClassLoader 加载后,JVM 会在方法区(元空间)创建一个对应的 java.lang.Class 对象,其中包含了该类的完整结构信息(从 .class 文件解析而来)。反射 API 就是通过这个 Class 对象来获取字段、方法、构造器等元数据的。反射调用方法时,前 15 次使用JNI(Java Native Interface) 实现的 NativeMethodAccessorImpl,之后会动态生成字节码切换为 GeneratedMethodAccessorXxx(称为膨胀 Inflation),以提升后续调用性能。反射调用比直接调用慢的原因主要有:无法内联优化、需要检查访问权限、参数装箱拆箱、方法查找等额外开销。
反射是很多框架的基础(Spring 的依赖注入、MyBatis 的 ORM 映射等)。
疑惑:反射看起来就是"在运行时获取类信息",为什么说它很重要?
在编译时我们就知道一个类有什么字段和方法,为什么还需要在运行时去"反射"获取?直接调用不就行了吗?
答疑:反射的价值在于解耦。当你编写框架代码时,你不知道用户会写什么类。比如 Spring 的 @Autowired 需要自动注入依赖,但 Spring 框架在编写时不可能知道用户的 UserService 类有哪些字段。唯一的办法就是在运行时通过反射扫描类信息。
论证:考虑以下对比:
// 没有反射:硬编码,每加一个类就要改代码
public Object createBean(String className) {
if (className.equals("UserService")) return new UserService();
if (className.equals("OrderService")) return new OrderService();
// 每新增一个类都要加 if-else...
}
// 使用反射:完全通用
public Object createBean(String className) throws Exception {
Class<?> cls = Class.forName(className);
return cls.getDeclaredConstructor().newInstance();
// 无论什么类,同一套代码都能处理
}
2
3
4
5
6
7
8
9
10
11
12
13
结果展示:反射让框架代码与业务代码完全解耦。Spring、Hibernate、Jackson、JUnit 等框架的核心都建立在反射之上。没有反射,就没有现代 Java 框架生态。当然,反射的代价是性能和类型安全的损失,因此在业务代码中不建议滥用。
Class 对象的加载时机:
// Class 对象在类第一次被使用时由 ClassLoader 加载
// 以下操作会触发类加载:
// 1. new 关键字
// 2. 访问静态成员
// 3. Class.forName()
// 4. 反射操作
// 5. 子类加载时会先加载父类
// 类加载过程:加载 → 链接(验证→准备→解析)→ 初始化
// 加载:读取 .class 文件,创建 Class 对象
// 验证:校验字节码格式的正确性
// 准备:为静态变量分配内存,赋默认值
// 解析:将符号引用转为直接引用
// 初始化:执行 <clinit>()(静态变量赋值和静态代码块)
2
3
4
5
6
7
8
9
10
11
12
13
14
# 15.3.2 获取Class对象
// 方式1:类名.class
Class<Account> cls1 = Account.class;
// 方式2:对象.getClass()
Account acc = new Account();
Class<?> cls2 = acc.getClass();
// 方式3:Class.forName(最灵活,类名可以是字符串变量)
Class<?> cls3 = Class.forName("com.bank.model.Account");
System.out.println(cls1 == cls2); // true(同一个Class对象)
2
3
4
5
6
7
8
9
10
11
三种获取 Class 对象方式的区别:
| 方式 | 是否触发类初始化 | 使用场景 |
|---|---|---|
类名.class | 不会触发 | 编译时已知类名 |
对象.getClass() | 类已初始化 | 运行时有对象实例 |
Class.forName() | 会触发初始化 | 运行时类名是字符串变量(最灵活) |
// Class.forName 会触发类的初始化(执行静态代码块)
// 这就是 JDBC 加载驱动的原理:
Class.forName("com.mysql.cj.jdbc.Driver");
// MySQL Driver 类有一个静态代码块:
// static { DriverManager.registerDriver(new Driver()); }
// Class.forName 触发了这个静态代码块的执行
// 如果不想触发初始化:
Class<?> cls = Class.forName("com.example.MyClass", false, classLoader);
// 第二个参数 false 表示不初始化
2
3
4
5
6
7
8
9
10
基本类型和数组的 Class 对象:
// 基本类型也有 Class 对象
Class<Integer> intClass = int.class; // 注意:int.class != Integer.class
Class<Void> voidClass = void.class; // void 也有 Class 对象
// 数组也有 Class 对象
Class<?> arrClass = int[].class;
System.out.println(arrClass.getName()); // [I
Class<?> arrClass2 = String[].class;
System.out.println(arrClass2.getName()); // [Ljava.lang.String;
// 判断是否为数组
System.out.println(arrClass.isArray()); // true
2
3
4
5
6
7
8
9
10
11
12
# 15.3.3 获取类信息
Class<Account> cls = Account.class;
// 获取类名
System.out.println(cls.getName()); // com.bank.model.Account
System.out.println(cls.getSimpleName()); // Account
// 获取所有公共方法(包括继承的)
Method[] methods = cls.getMethods();
for (Method m : methods) {
System.out.println(m.getName());
}
// 获取所有声明的字段(包括私有的)
Field[] fields = cls.getDeclaredFields();
for (Field f : fields) {
System.out.println(f.getName() + " : " + f.getType().getSimpleName());
}
// 获取构造方法
Constructor<?>[] constructors = cls.getConstructors();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
getMethods() vs getDeclaredMethods() 的区别:
| 方法 | 范围 | 包含私有 | 包含继承的 |
|---|---|---|---|
getMethods() | public 方法 | 否 | 是 |
getDeclaredMethods() | 所有方法 | 是 | 否 |
getFields() | public 字段 | 否 | 是 |
getDeclaredFields() | 所有字段 | 是 | 否 |
规律:带
Declared的方法可以获取私有成员,但不包含继承的;不带Declared的只能获取 public 成员,但包含继承的。
# 15.3.4 创建对象
Class<Account> cls = Account.class;
// 通过无参构造创建
Account acc1 = cls.getDeclaredConstructor().newInstance();
// 通过有参构造创建
Constructor<Account> constructor = cls.getDeclaredConstructor(String.class, double.class);
Account acc2 = constructor.newInstance("张三", 1000.0);
2
3
4
5
6
7
8
# 15.3.5 调用方法
Class<Account> cls = Account.class;
Account acc = new Account("张三", 1000);
// 获取方法并调用
Method depositMethod = cls.getMethod("deposit", double.class);
depositMethod.invoke(acc, 500.0); // 等价于 acc.deposit(500.0)
// 调用私有方法
Method privateMethod = cls.getDeclaredMethod("validateAmount", double.class);
privateMethod.setAccessible(true); // 允许访问私有方法
Object result = privateMethod.invoke(acc, 100.0);
2
3
4
5
6
7
8
9
10
11
invoke 方法的参数传递:
// 无参方法
Method getName = cls.getMethod("getName");
String name = (String) getName.invoke(acc);
// 有多个参数的方法
Method transfer = cls.getMethod("transfer", Account.class, double.class);
transfer.invoke(acc, targetAcc, 500.0);
// 静态方法(第一个参数传 null)
Method valueOf = Integer.class.getMethod("valueOf", String.class);
Integer num = (Integer) valueOf.invoke(null, "123");
// 可变参数方法(需要特殊处理)
Method format = String.class.getMethod("format", String.class, Object[].class);
String result2 = (String) format.invoke(null, "Hello %s, age %d", new Object[]{"Alice", 25});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 15.3.6 访问字段
Class<Account> cls = Account.class;
Account acc = new Account("张三", 1000);
// 访问私有字段
Field balanceField = cls.getDeclaredField("balance");
balanceField.setAccessible(true); // 允许访问私有字段
double balance = (double) balanceField.get(acc); // 读取
System.out.println("余额:" + balance);
balanceField.set(acc, 9999.0); // 修改
System.out.println("修改后余额:" + acc.getBalance());
2
3
4
5
6
7
8
9
10
11
12
# 15.3.7 综合案例:对象检查器
本案例综合运用反射机制的 Class 对象、构造器、方法和字段操作。
import java.lang.reflect.*;
/**
* 对象检查器 —— 反射机制综合案例
* 知识点:获取Class、遍历字段/方法、创建对象、调用方法、访问私有成员
*/
class Person {
private String name;
private int age;
public Person() { this("未知", 0); }
public Person(String name, int age) {
this.name = name;
this.age = age;
}
private String getInfo() {
return name + "(" + age + "岁)";
}
public void greet() {
System.out.println(" 你好,我是" + name);
}
}
public class ObjectInspector {
static void inspect(Class<?> clazz) {
System.out.println(" 类名: " + clazz.getName());
System.out.println(" 构造器:");
for (Constructor<?> c : clazz.getDeclaredConstructors()) {
System.out.println(" " + c);
}
System.out.println(" 字段:");
for (Field f : clazz.getDeclaredFields()) {
System.out.printf(" %s %s %s%n",
Modifier.toString(f.getModifiers()), f.getType().getSimpleName(), f.getName());
}
System.out.println(" 方法:");
for (Method m : clazz.getDeclaredMethods()) {
System.out.printf(" %s %s %s()%n",
Modifier.toString(m.getModifiers()), m.getReturnType().getSimpleName(), m.getName());
}
}
public static void main(String[] args) throws Exception {
System.out.println("===== 对象检查器 =====\n");
// 1. 获取 Class 对象的三种方式
System.out.println("--- 获取 Class ---");
Class<?> c1 = Person.class;
Class<?> c2 = new Person().getClass();
Class<?> c3 = Class.forName("Person");
System.out.println(" 三种方式同一个: " + (c1 == c2 && c2 == c3));
// 2. 检查类结构
System.out.println("\n--- 类结构检查 ---");
inspect(Person.class);
// 3. 反射创建对象
System.out.println("\n--- 反射创建对象 ---");
Constructor<?> ctor = Person.class.getDeclaredConstructor(String.class, int.class);
Person p = (Person) ctor.newInstance("张三", 25);
p.greet();
// 4. 反射访问私有字段
System.out.println("\n--- 访问私有字段 ---");
Field nameField = Person.class.getDeclaredField("name");
nameField.setAccessible(true); // 突破 private
System.out.println(" name = " + nameField.get(p));
nameField.set(p, "李四");
System.out.println(" 修改后 name = " + nameField.get(p));
// 5. 反射调用私有方法
System.out.println("\n--- 调用私有方法 ---");
Method getInfo = Person.class.getDeclaredMethod("getInfo");
getInfo.setAccessible(true);
System.out.println(" getInfo() = " + getInfo.invoke(p));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# 15.3.8 反射机制训练题
训练1:编写一个通用的 toString 方法:String reflectToString(Object obj),通过反射读取对象所有字段的名称和值,拼接成 "ClassName{field1=value1, field2=value2}" 的格式。
训练2:编写一个方法 Object deepCopy(Object obj),通过反射实现对象的深拷贝(创建新对象,逐字段复制值)。只需支持基本类型和 String 类型的字段。
训练3:通过反射获取 java.util.ArrayList 类的所有构造方法,打印它们的参数类型。然后用反射创建一个初始容量为 100 的 ArrayList。
思考:反射可以调用私有构造方法(setAccessible(true)),这意味着单例模式可能被破坏。如何防止反射破坏单例?(提示:枚举单例。)
# 15.4 注解和反射结合
反射可以在运行时读取注解信息:
Class<Account> cls = Account.class;
// 读取类上的注解
if (cls.isAnnotationPresent(ApiInfo.class)) {
ApiInfo info = cls.getAnnotation(ApiInfo.class);
System.out.println("作者:" + info.author());
System.out.println("版本:" + info.version());
System.out.println("描述:" + info.description());
}
// 读取方法上的注解
for (Method method : cls.getMethods()) {
if (method.isAnnotationPresent(ApiInfo.class)) {
ApiInfo info = method.getAnnotation(ApiInfo.class);
System.out.println(method.getName() + ": " + info.description());
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
这就是 Spring 框架实现 @Autowired、@RequestMapping 等注解功能的底层原理。
# 15.4.1 综合案例:简易字段校验器
本案例用自定义注解+反射实现字段校验,展示注解和反射结合的经典用法。
import java.lang.annotation.*;
import java.lang.reflect.*;
/**
* 简易字段校验器 —— 注解+反射综合案例
* 知识点:自定义注解定义、反射读取注解、运行时处理
*/
// 自定义注解:非空校验
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface NotNull {
String message() default "不能为空";
}
// 自定义注解:范围校验
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface Range {
int min();
int max();
String message() default "超出范围";
}
class User {
@NotNull(message = "用户名不能为空")
String name;
@Range(min = 0, max = 150, message = "年龄必须在0-150之间")
int age;
@NotNull
String email;
User(String name, int age, String email) {
this.name = name;
this.age = age;
this.email = email;
}
}
class Validator {
static void validate(Object obj) throws Exception {
Class<?> clazz = obj.getClass();
for (Field field : clazz.getDeclaredFields()) {
field.setAccessible(true);
Object value = field.get(obj);
// 处理 @NotNull
if (field.isAnnotationPresent(NotNull.class)) {
NotNull nn = field.getAnnotation(NotNull.class);
if (value == null || (value instanceof String && ((String) value).isEmpty())) {
System.out.println(" ✗ " + field.getName() + ": " + nn.message());
continue;
}
}
// 处理 @Range
if (field.isAnnotationPresent(Range.class)) {
Range range = field.getAnnotation(Range.class);
if (value instanceof Integer) {
int val = (Integer) value;
if (val < range.min() || val > range.max()) {
System.out.println(" ✗ " + field.getName() + ": " + range.message()
+ " (值=" + val + ")");
continue;
}
}
}
System.out.println(" ✓ " + field.getName() + " = " + value);
}
}
}
public class FieldValidatorDemo {
public static void main(String[] args) throws Exception {
System.out.println("===== 简易字段校验器 =====\n");
System.out.println("--- 有效用户 ---");
Validator.validate(new User("张三", 25, "zhang@mail.com"));
System.out.println("\n--- 无效用户 ---");
Validator.validate(new User("", 200, null));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
运行结果:
===== 简易字段校验器 =====
--- 有效用户 ---
✓ name = 张三
✓ age = 25
✓ email = zhang@mail.com
--- 无效用户 ---
✗ name: 用户名不能为空
✗ age: 年龄必须在0-150之间 (值=200)
✗ email: 不能为空
2
3
4
5
6
7
8
9
10
11
# 15.4.2 注解反射结合训练题
训练1:创建 @Range(min, max) 注解用于标注 int 字段的合法范围,编写 validate(Object obj) 方法通过反射读取注解并校验字段值是否在范围内。
训练2:创建 @JsonField(name) 注解,编写 toJson(Object obj) 方法,将对象转换为简单的 JSON 字符串。字段名使用注解指定的名称,没有注解的字段使用原始字段名。
思考:Spring Boot 的 @SpringBootApplication 注解背后包含了 @Configuration、@EnableAutoConfiguration、@ComponentScan 三个注解。这种"注解叠加"的机制在 Java 中如何实现?
# 15.5 反射的优缺点
| 优点 | 缺点 |
|---|---|
| 运行时动态操作,灵活性极高 | 性能开销大(比直接调用慢) |
| 是框架和库的基础 | 破坏封装性(可以访问私有成员) |
| 实现通用工具和组件 | 代码可读性降低 |
| 支持插件化架构 | 编译期无法检查类型安全 |
建议:在业务代码中尽量少用反射,在框架和底层工具开发中合理使用。
反射性能优化策略:
// 策略1:缓存 Class、Method、Field 对象(避免重复查找)
private static final Map<String, Method> METHOD_CACHE = new ConcurrentHashMap<>();
public static Object invoke(Object obj, String methodName) throws Exception {
Method method = METHOD_CACHE.computeIfAbsent(methodName, name -> {
try {
Method m = obj.getClass().getDeclaredMethod(name);
m.setAccessible(true);
return m;
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
});
return method.invoke(obj);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 策略2:使用 MethodHandle(JDK 7+,性能接近直接调用)
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle mh = lookup.findVirtual(
String.class, "length", MethodType.methodType(int.class));
int len = (int) mh.invoke("Hello"); // 5
// MethodHandle 可以被 JIT 内联优化,性能远优于反射
2
3
4
5
6
7
8
9
10
// 策略3:使用 VarHandle(JDK 9+,替代反射访问字段)
import java.lang.invoke.VarHandle;
import java.lang.invoke.MethodHandles;
// VarHandle 提供了类型安全的字段访问,性能优于反射
2
3
4
5
反射性能对比(大致数据):
| 操作方式 | 相对耗时 |
|---|---|
| 直接调用 | 1x(基准) |
| MethodHandle | ~1.5x |
| 反射(缓存后) | ~5-10x |
| 反射(未缓存) | ~50-100x |
# 15.5.1 综合案例:反射性能测试
本案例对比直接调用、反射调用和 MethodHandle 的性能差异。
import java.lang.invoke.*;
import java.lang.reflect.*;
/**
* 反射性能测试 —— 反射优缺点综合案例
* 知识点:反射开销、缓存优化、MethodHandle
*/
public class ReflectionBenchmark {
public int add(int a, int b) { return a + b; }
public static void main(String[] args) throws Throwable {
System.out.println("===== 反射性能测试 =====\n");
ReflectionBenchmark obj = new ReflectionBenchmark();
int iterations = 1_000_000;
// 1. 直接调用
long start = System.nanoTime();
for (int i = 0; i < iterations; i++) {
obj.add(1, 2);
}
long directTime = System.nanoTime() - start;
// 2. 反射调用(每次查找)
start = System.nanoTime();
for (int i = 0; i < iterations; i++) {
Method m = obj.getClass().getMethod("add", int.class, int.class);
m.invoke(obj, 1, 2);
}
long reflectTime = System.nanoTime() - start;
// 3. 反射调用(缓存 Method)
Method cached = obj.getClass().getMethod("add", int.class, int.class);
cached.setAccessible(true);
start = System.nanoTime();
for (int i = 0; i < iterations; i++) {
cached.invoke(obj, 1, 2);
}
long cachedTime = System.nanoTime() - start;
// 4. MethodHandle
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle mh = lookup.findVirtual(ReflectionBenchmark.class, "add",
MethodType.methodType(int.class, int.class, int.class));
start = System.nanoTime();
for (int i = 0; i < iterations; i++) {
mh.invoke(obj, 1, 2);
}
long mhTime = System.nanoTime() - start;
System.out.printf(" 直接调用 : %6.2f ms (1.0x)%n", directTime / 1e6);
System.out.printf(" 反射(未缓存) : %6.2f ms (%.1fx)%n", reflectTime / 1e6, (double) reflectTime / directTime);
System.out.printf(" 反射(缓存) : %6.2f ms (%.1fx)%n", cachedTime / 1e6, (double) cachedTime / directTime);
System.out.printf(" MethodHandle : %6.2f ms (%.1fx)%n", mhTime / 1e6, (double) mhTime / directTime);
System.out.println("\n--- 使用建议 ---");
System.out.println(" 框架开发: 合理使用反射(缓存 Method/Field)");
System.out.println(" 高性能场景: 考虑 MethodHandle");
System.out.println(" 业务代码: 尽量避免反射");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# 15.5.2 反射优缺点训练题
训练1:反射最大的性能开销来自哪里?为什么缓存 Method 对象能显著提升性能?
训练2:反射可以访问 private 成员,这是否违反了面向对象的封装原则?框架(如 Spring)为什么需要这个能力?
思考:Java 9 引入了模块系统(JPMS),对反射的 setAccessible(true) 做了更严格的限制。这对框架开发有什么影响?