第13章:注解、反射与元编程
本章讲 Java 里的"元编程"——让代码能"看"和"标记"其他代码。
📌 代码说明:标注"可复制运行"的代码块包含完整的
main方法。没标注的是概念示意片段。
一个故事开头
你从第8章开始就在写 @Override。你大概知道它是"告诉编译器我在重写父类方法"。但你有没想过——
@Override 到底是什么?它不是 Java 的关键字,那为什么 Java 编译器认识它?
答案是:@Override 是一个注解(Annotation)。注解就像贴在代码上的便利贴——上面写着"这段代码有什么属性",编译器或者运行环境可以读到这个便利贴,然后做相应的事。
顺着这个思路往下想:如果我能自己写便利贴,写一个 @LogExecutionTime 贴在方法上,让程序自动记录每个方法的执行时间——这不就是"元编程"了吗?
1. JDK 内置的三个注解
// @Override——告诉编译器"我在重写父类方法"
// 写错方法名时编译器直接报错,而不是默默当成新方法
@Override
void make() { ... }
// @Deprecated——标记这个方法"过时了,别用了"
// 谁调用它编译时会收到警告
@Deprecated
public void oldMethod() { ... }
// @SuppressWarnings——"我知道这里有警告,别烦我"
@SuppressWarnings("unchecked")
public void doSomething() { ... }2. 定义自己的注解
最基本的注解
import java.lang.annotation.*;
// @interface 定义注解——注意不是 interface
public @interface Loggable {
}
// 使用
@Loggable
public void makeTea() { ... }带参数的注解
public @interface Author {
String name(); // 注解参数——像方法声明
String date();
String description() default "无描述"; // 默认值
}
// 使用
@Author(name = "团子", date = "2026-06-22")
public class MilkTeaOrderSystem {
// ...
}
// 只有一个参数时习惯叫 value
public @interface Version {
int value(); // 如果有且仅有一个参数叫 value,使用时可省略参数名
}
@Version(2) // 等价于 @Version(value = 2)
public class SomeClass { }元注解——注解的注解
定义注解时,你要告诉 Java 这个注解贴在哪里、什么时候读取、能不能重复贴。
import java.lang.annotation.*;
@Target(ElementType.METHOD) // 这个注解只能贴在方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时可以通过反射读取
public @interface LogExecutionTime {
}| 元注解 | 作用 | 常用值 |
|---|---|---|
@Target | 这个注解能贴在哪 | METHOD、TYPE(类)、FIELD、PARAMETER |
@Retention | 保留到什么阶段 | SOURCE(编译完丢弃)、CLASS(类文件里有)、RUNTIME(运行时也能读) |
3. 反射——让代码"看"自己
反射(Reflection)就是程序运行时查看自己和操作自己的能力。
// 可复制运行,保存为 ReflectionDemo.java
import java.lang.reflect.*;
public class ReflectionDemo {
public static void main(String[] args) throws Exception {
// 获取 String 类的 Class 对象
Class<?> clazz = String.class;
System.out.println("类名: " + clazz.getName());
System.out.println("父类: " + clazz.getSuperclass().getName());
System.out.println("\n方法列表:");
for (Method m : clazz.getMethods()) {
System.out.println(" " + m.getName() + "()");
if (m.getParameterCount() > 0) {
System.out.print(" 参数: ");
for (Class<?> p : m.getParameterTypes()) {
System.out.print(p.getSimpleName() + " ");
}
System.out.println();
}
}
}
}输出(节选):
类名: java.lang.String
父类: java.lang.Object
方法列表:
length()
isEmpty()
charAt()
参数: int
...💥 拆了它:用反射绕过 private
// 可复制运行,保存为 PrivateAccess.java
import java.lang.reflect.*;
class BankAccount {
private double balance = 1000;
private void secretMethod() {
System.out.println("你发现了我!余额: " + balance);
}
}
public class PrivateAccess {
public static void main(String[] args) throws Exception {
BankAccount account = new BankAccount();
// 获取私有字段并修改
Field balanceField = BankAccount.class.getDeclaredField("balance");
balanceField.setAccessible(true); // 取消 Java 的访问检查
double oldBalance = (double) balanceField.get(account);
System.out.println("原始余额: " + oldBalance);
balanceField.set(account, 999999.99);
System.out.println("新余额: " + balanceField.get(account));
// 调用私有方法
Method secret = BankAccount.class.getDeclaredMethod("secretMethod");
secret.setAccessible(true);
secret.invoke(account); // 你发现了我!余额: 999999.99
}
}反射可以破坏封装。 日常开发中不要这样做——它破坏了代码的本来意图。但框架(Spring、Hibernate)广泛用这个机制来做依赖注入、ORM 映射等。
4. 注解 + 反射——让注解真正干活
光有注解只是贴了便利贴。要让它产生效果,必须配合反射去读取它。
// 可复制运行,保存为 AnnotationProcessor.java
import java.lang.annotation.*;
import java.lang.reflect.*;
// 定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface LogExecutionTime {
}
class TeaMaker {
@LogExecutionTime
public void makeTea() throws InterruptedException {
System.out.println("制作奶茶中...");
Thread.sleep(500); // 模拟制作耗时
}
public void cleanUp() {
System.out.println("清理台面");
}
}
public class AnnotationProcessor {
public static void main(String[] args) throws Exception {
TeaMaker maker = new TeaMaker();
// 遍历 TeaMaker 的所有方法
for (Method method : TeaMaker.class.getDeclaredMethods()) {
// 检查方法上有没有 @LogExecutionTime
if (method.isAnnotationPresent(LogExecutionTime.class)) {
System.out.println("执行 " + method.getName() + "...");
long start = System.nanoTime();
method.invoke(maker);
long end = System.nanoTime();
long ms = (end - start) / 1_000_000;
System.out.println(method.getName() + " 耗时: " + ms + "ms");
} else {
System.out.println("普通调用 " + method.getName());
method.invoke(maker);
}
}
}
}输出:
执行 makeTea...
制作奶茶中...
makeTea 耗时: ~500ms
普通调用 cleanUp...
清理台面这个模式就是 Spring 的雏形:
- 定义注解(
@Autowired、@Transactional) - 贴到类或方法上
- 框架在启动时用反射扫描注解,做对应的处理
5. 实际世界里的注解
你在 Spring 等项目里看到的注解,本质上就是这样工作的:
// Spring Boot 中你以后会写的代码
@SpringBootApplication
@RestController
public class MyController {
@Autowired
private OrderService orderService;
@GetMapping("/orders")
public List<Order> getOrders() {
return orderService.findAll();
}
}Spring 在启动时做的事情:
- 用反射扫描所有类,发现有
@RestController的类——注册为 HTTP 控制器 - 发现
@Autowired字段——自动注入对应的对象 - 发现
@GetMapping方法——注册路由
注解只是标记。真正做事的是读注解的框架代码。
本章小结
- 注解 = 代码上的便利贴——给类、方法、字段打标签
- JDK 内置:
@Override、@Deprecated、@SuppressWarnings - 自定义注解:用
@interface,可以带参数和默认值 - 元注解:
@Target(贴在哪)、@Retention(保留多久) - 反射:运行时查看类的结构(方法、字段、注解)
- 反射 + 注解 = 元编程:框架读取注解,自动执行逻辑
- 实际应用:Spring 的
@Autowired、@GetMapping等
✅ 验收标准
完成本章后,你应该能:
- [ ] 使用常用内置注解(@Override, @Deprecated 等)
- [ ] 定义自己的注解,包括元注解配置
- [ ] 通过反射获取类的方法/字段/注解信息
- [ ] 注解 + 反射结合实现简单逻辑
- [ ] 理解 Spring 注解的基本原理
📌 常见卡点
- 反射能访问私有成员但会导致封装破坏
- 注解没有继承性——子类不继承父类注解
- 反射性能开销不小——别在频繁调用的路径上用
- 未设置 @Retention(RUNTIME) 的注解在运行时读不到
🔜 现在不需要理解
InvocationHandler和动态代理ClassLoader自定义加载器- 编译期注解处理(APT)——框架开发者才需要
🧪 练习
1. 定义注解:定义一个 @Important 注解,有一个 String level() 参数,默认值为 "HIGH"。用 @Retention(RUNTIME) 和 @Target(METHOD)。
2. 读取注解:写一个类,有三个方法——两个贴上 @Important(不同级别),一个不贴。用反射遍历方法,打印出所有 @Important 的方法名和级别。
3. 反射调用:用反射调用一个私有方法 private void secret(),打印它的输出。