首页 / Java 入门教程 / 注解(Annotation)

Java 入门教程

注解(Annotation)

本教程共 100 篇 · 第 51 篇 · 更新于 2026-08-05 · 约 6 分钟阅读

JavaJava 入门教程注解Annotation元注解自定义注解

本节目标:理解注解是「代码的标签」而非逻辑,认识常用内置注解和元注解,并学会用 @interface 定义自己的注解及配置参数。

注解到底是什么

注解(Annotation)是贴在类、方法、字段、参数前的一种特殊「注释」,比如大家都见过的:

@Override
public String toString() {
    return "...";
}

它和普通注释最大的区别:注释会被编译器直接丢掉;注解会被编译器写进 .class 文件,之后可以被工具或程序在编译期、运行期读出来,去做一些事。

Note

关键认知:注解本身不改变代码逻辑。它只是一份「元数据(标签)」。至于看到这个标签后做什么,完全由读取它的工具决定——编译器可能拿它做检查,@Override 就是;框架可能拿它做配置,Spring 就是。

常用的内置注解

Java 自带几个你一定会碰到的注解:

  • @Override:告诉编译器「我是故意重写父类方法的」。如果签名写错(其实没重写成功),编译器立刻报错。强烈建议每次重写都加。
  • @Deprecated:标记「这个方法/类已过时,不推荐再用」。别人使用时编译器会给出删除线警告,但仍能编译运行(用于温和地提示迁移)。
  • @SuppressWarnings("警告名"):让编译器「别提示某类警告」,比如 @SuppressWarnings("unused") 忽略未使用变量的警告。
  • @FunctionalInterface:我们在 Lambda 那章见过,标注「这是个函数式接口」,编译器会帮你检查是否只有一个抽象方法。
@Deprecated
public void oldMethod() { }     // 调用处会出现删除线提示

@Override
public String toString() { return ""; }   // 写错方法名编译器立刻报错
Warning

@Deprecated 是「提醒别用」的标签,不是「允许用废弃 API」的通行证。本教程主线从不使用已废弃的 API(如 VectorHashtablenew Integer()),只是借这个注解说明它的用途。

元注解:注解的注解

定义自己的注解时,要用「元注解」来描述这个新注解能贴在哪、活到什么时候。最常用的几个:

  • @Target:规定注解能用在哪里(类?方法?字段?参数?)。取值如 ElementType.TYPEMETHODFIELDPARAMETER
  • @Retention:规定注解保留到哪个阶段。常用 RetentionPolicy.RUNTIME(运行期还能读到)和 SOURCE(编译完就丢)、CLASS(留到 class 但进不了 JVM)。
  • @Documented:让注解出现在生成的 API 文档里。
  • @Inherited:允许子类继承父类的注解。
  • @Repeatable(Java 8 引入):允许同一个地方贴多次该注解。
Tip

绝大多数「自己写的、要被程序运行时读取」的注解,都要配 @Retention(RetentionPolicy.RUNTIME)。忘记它,运行期就 getAnnotation 不到,这是高频踩坑点。

三种保留策略对照

@Retention 只有三个取值,差别就是「注解活多久」:

策略存在于 .class 文件运行期可反射读取典型用途
SOURCE@Override@SuppressWarnings 这类只给编译器看的
CLASS字节码增强工具、编译期织入
RUNTIMESpring、JUnit 等框架靠反射读的注解

不写 @Retention 时默认是 CLASS——这正是「注解明明贴了,getAnnotation() 却返回 null」的头号原因。

@Target 的常用取值也顺手记一下:

取值能贴在哪
TYPE类、接口、枚举、record
METHOD方法
FIELD字段、枚举常量
PARAMETER方法参数
CONSTRUCTOR构造方法
ANNOTATION_TYPE另一个注解(即元注解)

贴错位置编译器立刻拦下:

error: annotation type not applicable to this kind of declaration

用 @interface 自定义注解

自定义注解用 @interface 声明。里面写的是「配置参数」(看起来像方法,其实是参数):

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;
}

配置参数的类型限制

注解的参数不是随便什么类型都行,只允许:

  • 所有基本类型(intlongboolean 等);
  • String
  • Class
  • 枚举类型;
  • 其他注解类型;
  • 以及以上类型的数组

因为参数值必须在写代码时就定死(是常量),所以这些限制保证了注解在编译期就能确定所有参数。

value 的简写

如果参数名恰好叫 value,且只传这一个参数时,可以省略名字:

public @interface Check {
    int value();
}

@Check(99)          // 等价于 @Check(value = 99)
public int score;

如果参数不止一个,或者名字不是 value,就不能省略:

@Range(min = 1, max = 20)   // 必须写参数名

另外,一个注解里什么都不写,表示全部用默认值。

完整可运行示例(定义 + 使用)

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() { }
}
Note

上面只是「贴标签」。要让标签真正起作用(比如扫描所有 @Todo 方法生成待办清单),得用下一章的反射去读取它们。注解 + 反射,才是框架工作的标准套路。

注解都用在哪

除了编译器自带的 @Override 等,注解在现实项目里有几类典型用途:

  • 框架配置:Spring 的 @Component@Autowired,JUnit 的 @Test,都是「贴标签 + 框架读标签」的范式;
  • 代码检查:静态分析工具靠自定义注解标记「哪些方法只允许内部调用」「哪些字段不能为空」;
  • 文档生成:配合 @Documented,让注解信息进入 API 文档。

记住一句总结:注解负责「声明意图」,真正「落实动作」的,是读取它的程序(通常是编译器或框架,靠反射实现)。没有读取方,注解就只是一行无害的标记。

常见疑问

注解参数能用 null 作默认值吗?

不能。注解参数值必须是编译期常量,null 不在允许之列:

error: attribute value must be constant

想表达「没填」,惯用做法是给一个空字符串或负数当哨兵值,比如 String name() default "";

没写默认值的参数,使用时可以不传吗?

不行,必须传。少传一个就报:

error: annotation @Range is missing a default value for the element 'min'

所以定义注解时,能给默认值就给,调用方会舒服很多。

@Inherited 为什么有时候「没继承过来」?

它只对类上的注解生效,且只作用于 extends 这条链。接口上的注解不会被实现类继承,方法上的注解也不会被重写方法继承——这是 @Inherited 最容易被误解的地方。

同一个注解想在一个地方贴两次?

给注解加 @Repeatable,并配一个「容器注解」:

@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)

下一章我们进入反射,看看如何「在运行期拿到类的所有信息」,这正是注解、框架能工作的底层能力。