📝 一句话概括
注释是给人看的,注解是给程序看的——一张贴在代码上的标签。这节学会认标签(内置三大件)、造标签(自定义注解 + 元注解),为 9.5 反射读注解备好货。
1. 引入:注释 vs 注解,一字之差
- 注释:写给人看的,编译器直接扔掉,程序根本不知道它存在。
- 注解(Annotation):写给程序看的标签,能被编译器检查、能被反射读到——它是活的信息。
一句话记住:注解 = 标签,代码是商品。至于标签管不管用,得看有没有"人"来读它。
@Deprecated // ← 标签:这玩意儿过时了
public class Student {
@Override // ← 标签:我重写了父类方法,编译器帮我盯着
public String toString() { ... }
}
2. 注解的三大读者
- 编译器:
@Override(重写检查)、@Deprecated(过时警告)、@SuppressWarnings("all")(关警告)。 - 运行时程序(反射):JUnit 的
@Test、Spring 的@Component——框架扫描注解决定干啥,这是注解最大的价值。 - 人和工具:生成文档、IDE 灰条提示。
Java 内置三大注解速查
| 注解 | 贴在哪 | 作用 | 旧账 |
|---|---|---|---|
@Override |
方法 | 声明"我重写了父类方法",拼错方法名直接编译报错 | 4.1 继承的重写 |
@Deprecated |
任意 | 标记过时,调用处画删除线给警告 | 6.6 的过时 Date 构造 |
@SuppressWarnings("all") |
任意 | 压制警告 | 6.5 的 BigDecimal 类型警告 |
📝 @Override 为什么值得贴?
不贴也能重写——但贴上后,你把 toString 敲成 tostring,编译器当场报错;不贴则悄悄变成一个新方法,重写失败你毫不知情。标签把"我以为"变成"编译器确认"。
3. 自定义注解:造自己的标签
public @interface MyAnno { // @interface —— 注意不是 interface
String name(); // "属性":长得像方法,其实是属性
int age() default 18; // 带 default 可以不赋值
}
使用与赋值规则:
@MyAnno(name = "波仔") // age 有默认值,可不写
@MyAnno(name = "波仔", age = 25) // 全赋值
@MyAnno("波仔") // 特例:唯一属性叫 value 时,可省略属性名
四条规矩:
- 关键字是
@interface,比普通接口多个@。 - "属性"长得像抽象方法(
String name();),但语义是属性声明,用name = "波仔"赋值。 - 不写 default 的属性,贴注解时必须赋值,否则编译报错。
- 属性类型有限制:八种基本类型、String、Class、枚举、注解、以及它们的数组——不能随便放个 Student 类型。
4. 元注解:管标签的标签
自定义注解贴出去之前,得先回答两个问题——能贴哪?活多久?这由两个元注解决定(元注解 = 贴在注解定义上的注解):
@Target(ElementType.METHOD) // 只能贴在方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时还在,反射能读
public @interface MyTest {
}
@Target(贴纸规格):TYPE类 /METHOD方法 /FIELD字段 /PARAMETER参数。贴错位置直接编译报错。@Retention(保质期):SOURCE编译完就扔(@Override就这档);CLASS进字节码但运行时丢(默认值,实战几乎不用);RUNTIME运行时还在——反射唯一能读到的档位。
⚠️ 本节最值钱的一句话
想让自己的注解被反射解析,@Retention(RetentionPolicy.RUNTIME) 必须写。忘了写就是默认 CLASS——标签贴得再认真,运行时早没了,反射一无所获。框架的注解清一色 RUNTIME。
5. 收网旧账
- 4.1 继承 / 方法重写:
@Override就是给重写上的保险丝。 - 4.7 函数式接口:
@FunctionalInterface是"函数式接口入场券"那个说法的官方版——贴了它,接口里敢加第二个抽象方法就编译报错。 - 6.6 日期类:过时的
Date构造方法上面的删除线,来源就是@Deprecated。 - 9.3 反射的常见操作:三件套能拿到 Class/Field/Method,而这三类对象都有
isAnnotationPresent()/getAnnotation()——标签贴上去,下节反射来撕。注解本身是死的,反射是读标签的那只手。 - 9.1 反射的应用场景:当时说"框架靠读配置和注解工作",配置式工厂已经写过(字符串类名),注解就是更优雅的"配置"。
6. 动手:Demo04 造一张自己的标签
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD) // 只能贴方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时还在 → 反射可读
public @interface MyTest {
String author() default "波仔";
}
public class Demo04 {
@MyTest
public void run() { System.out.println("贴了标签的方法"); }
public void walk() { System.out.println("没贴标签的方法"); }
public static void main(String[] args) throws Exception {
Class<?> c = Class.forName("Demo04");
for (Method m : c.getDeclaredMethods()) { // 9.3 的拆方法
if (m.isAnnotationPresent(MyTest.class)) { // 问:贴标签了吗?
System.out.println("执行 → " + m.getName());
m.invoke(c.getDeclaredConstructor().newInstance()); // 9.3 的造对象+调方法
}
}
}
}
运行只执行 run()——这二十几行就是 JUnit 的雏形:框架扫描贴了 @Test 的方法并执行,一模一样的套路。
三个 30 秒实验:
- 把
@Retention那行注释掉(默认 CLASS)再跑——什么都不输出:标签运行时没了,反射成了睁眼瞎。亲手体验"最值钱的一句话"。 - 把
@Target(ElementType.METHOD)改成@Target({ElementType.METHOD, ElementType.TYPE})——现在类上也能贴了,试试在Demo04类上贴@MyTest。 - 给
run()贴@MyTest(author = "墨池"),在循环里用m.getAnnotation(MyTest.class).author()打印出来——标签是带信息的,不只是"有/没有"。
7. 常见误区
⚠️ 五个坑
- ❌ 把注解属性当方法调用 ——
String name();是属性声明不是方法,赋值用name = "波仔",没有()。 - ❌ 没 default 的属性不赋值 ——
int age();不带 default,贴的时候必须写age = 25,少一个都编译不过。 - ❌ 随便一个属性都想省名字 —— 只有"唯一属性恰好叫
value"才能写@MyAnno("x");两个属性时省名直接报错。 - ❌ 忘写
@Retention(RUNTIME)还纳闷反射读不到 —— 默认 CLASS 档,运行时标签已消失。读不到注解,先查保质期。 - ❌ 贴了注解就等它自己生效 —— 注解是死标签,没"人"读它就是废纸。生效靠三选一:编译器检查 / 反射解析 / 工具生成。所谓"框架黑魔法",就是框架在替你读标签。
8. 可折叠自测
❓ Q1:注释和注解的本质区别?
注释给人看,编译后消失,程序不知道它存在;注解给程序看,是代码的元信息,能被编译器检查、被反射在运行时读到。
❓ Q2:`@interface` 里写 `String name();`——这是定义方法还是定义属性?赋值怎么写?
语法像抽象方法,语义是属性。赋值在贴注解时用 @MyAnno(name = "波仔"),带 default 的可省略。
❓ Q3:什么时候能写 `@MyAnno("x")` 而不用写属性名?
注解只有一个属性、且属性名恰好叫 value 时,可以省略属性名。
❓ Q4:`@Retention` 三档各自活到哪?反射哪档能读到?
SOURCE 活在源码(编译即扔)、CLASS 活到字节码(运行时丢,默认值)、RUNTIME 运行时还在。反射只能读 RUNTIME 档。
❓ Q5(毕业题):不看笔记,写出"迷你 JUnit"两件套:①定义 `@MyTest`(只能贴方法、RUNTIME 档);②反射扫描某类,把贴了 `@MyTest` 的方法全部执行,没贴的跳过。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTest { }
for (Method m : Class.forName("MyTestDemo").getDeclaredMethods()) {
if (m.isAnnotationPresent(MyTest.class)) {
m.invoke(Class.forName("MyTestDemo").getDeclaredConstructor().newInstance());
}
}
写出来,你就亲手造出了测试框架的心脏。
9. 下一步学什么
- ☑ 9.5 本章小结 ✅ —— 点此复习:四节串线、速查 12 条、模板 4 段、五大名坑、毕业大题"迷你测试框架"——第九章完结。
相关笔记
- 第九章 反射机制和注解/9.3 反射的常见操作:读标签的那只手——isAnnotationPresent / getAnnotation 都挂在三件套上
- 第九章 反射机制和注解/9.2 Class类和Class对象:注解也是 Class 对象"弹药库"里的一件(9.2 图里预留的那个格子)
- 第四章 面向对象(下)/4.7 函数式编程:
@FunctionalInterface入场券的官方身份 - 第四章 面向对象(下)/4.1 继承:
@Override保险丝的来源——方法重写 - 第一章 Java开发入门/1.4 Java程序的运行机制:SOURCE 档为什么编译完就扔——编译期与运行期的分界线