注解(Annotation)
本教程共 100 篇 · 第 51 篇 · 更新于 2026-08-05 · 约 6 分钟阅读
51. 注解(Annotation)
本节目标:理解注解是「代码的标签」而非逻辑,认识常用内置注解和元注解,并学会用
@interface定义自己的注解及配置参数。
注解到底是什么
注解(Annotation)是贴在类、方法、字段、参数前的一种特殊「注释」,比如大家都见过的:
@Override
public String toString() {
return "...";
}
```bash
它和普通注释最大的区别:注释会被编译器直接丢掉;**注解会被编译器写进 `.class` 文件**,之后可以被工具或程序在编译期、运行期读出来,去做一些事。
> [!NOTE]
> 关键认知:**注解本身不改变代码逻辑**。它只是一份「元数据(标签)」。至于看到这个标签后做什么,完全由读取它的工具决定——编译器可能拿它做检查,`@Override` 就是;框架可能拿它做配置,Spring 就是。
## 常用的内置注解
Java 自带几个你一定会碰到的注解:
- `@Override`:告诉编译器「我是故意重写父类方法的」。如果签名写错(其实没重写成功),编译器立刻报错。强烈建议每次重写都加。
- `@Deprecated`:标记「这个方法/类已过时,不推荐再用」。别人使用时编译器会给出删除线警告,但仍能编译运行(用于温和地提示迁移)。
- `@SuppressWarnings("警告名")`:让编译器「别提示某类警告」,比如 `@SuppressWarnings("unused")` 忽略未使用变量的警告。
- `@FunctionalInterface`:我们在 Lambda 那章见过,标注「这是个函数式接口」,编译器会帮你检查是否只有一个抽象方法。
```java
@Deprecated
public void oldMethod() { } // 调用处会出现删除线提示
@Override
public String toString() { return ""; } // 写错方法名编译器立刻报错
Warning
@Deprecated是「提醒别用」的标签,不是「允许用废弃 API」的通行证。本教程主线从不使用已废弃的 API(如Vector、Hashtable、new Integer()),只是借这个注解说明它的用途。
元注解:注解的注解
定义自己的注解时,要用「元注解」来描述这个新注解能贴在哪、活到什么时候。最常用的几个:
@Target:规定注解能用在哪里(类?方法?字段?参数?)。取值如ElementType.TYPE、METHOD、FIELD、PARAMETER。@Retention:规定注解保留到哪个阶段。常用RetentionPolicy.RUNTIME(运行期还能读到)和SOURCE(编译完就丢)、CLASS(留到 class 但进不了 JVM)。@Documented:让注解出现在生成的 API 文档里。@Inherited:允许子类继承父类的注解。@Repeatable(Java 8 引入):允许同一个地方贴多次该注解。
Tip绝大多数「自己写的、要被程序运行时读取」的注解,都要配
@Retention(RetentionPolicy.RUNTIME)。忘记它,运行期就getAnnotation不到,这是高频踩坑点。
三种保留策略对照
@Retention 只有三个取值,差别就是「注解活多久」:
| 策略 | 存在于 .class 文件 | 运行期可反射读取 | 典型用途 |
|---|---|---|---|
SOURCE | 否 | 否 | @Override、@SuppressWarnings 这类只给编译器看的 |
CLASS | 是 | 否 | 字节码增强工具、编译期织入 |
RUNTIME | 是 | 是 | Spring、JUnit 等框架靠反射读的注解 |
不写 @Retention 时默认是 CLASS——这正是「注解明明贴了,getAnnotation() 却返回 null」的头号原因。
@Target 的常用取值也顺手记一下:
| 取值 | 能贴在哪 |
|---|---|
TYPE | 类、接口、枚举、record |
METHOD | 方法 |
FIELD | 字段、枚举常量 |
PARAMETER | 方法参数 |
CONSTRUCTOR | 构造方法 |
ANNOTATION_TYPE | 另一个注解(即元注解) |
贴错位置编译器立刻拦下:
error: annotation type not applicable to this kind of declaration
```bash
## 用 @interface 自定义注解
自定义注解用 `@interface` 声明。里面写的是「配置参数」(看起来像方法,其实是参数):
```java
import java.lang.annotation.*;
@Target(ElementType.FIELD) // 只能贴在字段上
@Retention(RetentionPolicy.RUNTIME) // 运行期可读
public @interface Range {
int min() default 0; // 带默认值的参数
int max() default 255;
}
使用:
public class Person {
@Range(min = 1, max = 20)
public String name;
@Range(max = 10)
public String city;
}
```bash
## 配置参数的类型限制
注解的参数不是随便什么类型都行,只允许:
- 所有基本类型(`int`、`long`、`boolean` 等);
- `String`;
- `Class`;
- 枚举类型;
- 其他注解类型;
- 以及以上类型的**数组**。
因为参数值必须在写代码时就定死(是常量),所以这些限制保证了注解在编译期就能确定所有参数。
## value 的简写
如果参数名恰好叫 `value`,且**只传这一个参数**时,可以省略名字:
```java
public @interface Check {
int value();
}
@Check(99) // 等价于 @Check(value = 99)
public int score;
如果参数不止一个,或者名字不是 value,就不能省略:
@Range(min = 1, max = 20) // 必须写参数名
```bash
另外,一个注解里什么都不写,表示全部用默认值。
## 完整可运行示例(定义 + 使用)
```java
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Todo {
String value() default "";
String who() default "未分配";
}
public class Task {
@Todo("修复登录 bug")
public void login() { }
@Todo(value = "优化首页", who = "小红")
public void home() { }
}
```bash
> [!NOTE]
> 上面只是「贴标签」。要让标签真正起作用(比如扫描所有 `@Todo` 方法生成待办清单),得用下一章的**反射**去读取它们。注解 + 反射,才是框架工作的标准套路。
## 注解都用在哪
除了编译器自带的 `@Override` 等,注解在现实项目里有几类典型用途:
- 框架配置:Spring 的 `@Component`、`@Autowired`,JUnit 的 `@Test`,都是「贴标签 + 框架读标签」的范式;
- 代码检查:静态分析工具靠自定义注解标记「哪些方法只允许内部调用」「哪些字段不能为空」;
- 文档生成:配合 `@Documented`,让注解信息进入 API 文档。
记住一句总结:**注解负责「声明意图」,真正「落实动作」的,是读取它的程序(通常是编译器或框架,靠反射实现)**。没有读取方,注解就只是一行无害的标记。
## 常见疑问
**注解参数能用 `null` 作默认值吗?**
不能。注解参数值必须是编译期常量,`null` 不在允许之列:
```text
error: attribute value must be constant
想表达「没填」,惯用做法是给一个空字符串或负数当哨兵值,比如 String name() default "";。
没写默认值的参数,使用时可以不传吗?
不行,必须传。少传一个就报:
error: annotation @Range is missing a default value for the element 'min'
```java
所以定义注解时,能给默认值就给,调用方会舒服很多。
**`@Inherited` 为什么有时候「没继承过来」?**
它只对**类上的注解**生效,且只作用于 `extends` 这条链。接口上的注解不会被实现类继承,方法上的注解也不会被重写方法继承——这是 `@Inherited` 最容易被误解的地方。
**同一个注解想在一个地方贴两次?**
给注解加 `@Repeatable`,并配一个「容器注解」:
```java
@Repeatable(Todos.class)
public @interface Todo { String value(); }
public @interface Todos { Todo[] value(); }
之后就能连着写两个 @Todo("...") 了;读取时用 getAnnotationsByType(Todo.class) 一次拿全。
小结
- 注解是贴在代码上的元数据,本身不影响逻辑,由编译器或框架读取后产生效果。
- 常用内置注解:
@Override、@Deprecated、@SuppressWarnings、@FunctionalInterface。 - 元注解用来修饰自定义注解:
@Target(贴哪)、@Retention(活到哪)、@Documented、@Inherited、@Repeatable。 - 自定义注解用
@interface,参数类型限于基本类型、String、Class、枚举、注解及其数组。 - 名为
value的单参数可省略名字;运行时读取的注解务必加@Retention(RUNTIME)。
下一章我们进入反射,看看如何「在运行期拿到类的所有信息」,这正是注解、框架能工作的底层能力。