注解
概述
注解(Annotation)是 Java 中为代码附加元数据的机制:它本身不直接改变程序逻辑,而是为类、方法、字段、参数等提供「说明信息」。这些信息可在编译期被编译器使用(如 @Override 检查重写)、在运行时通过 反射 被框架读取(如 JUnit 的 @Test、Spring 的 @Component),从而实现配置、校验、代码生成等能力。
通俗理解:注解就像贴在代码上的「标签」——声明「这是什么」「有什么约束」;谁使用这些标签(编译器或反射),谁就按约定处理。学习注解有助于理解现代 Java 框架(如 Spring、JUnit、Lombok)的工作方式,并与 枚举、反射 形成完整知识链。
为什么需要注解
- 元数据与配置:在不侵入业务代码的前提下,声明类/方法的用途、约束或配置(如「这是测试方法」「这是 Web 控制器」)。
- 编译器检查:部分注解由编译器处理,如
@Override确保正确重写父类方法,@SuppressWarnings抑制警告。 - 运行时行为:框架通过反射读取注解,驱动行为(如 JUnit 执行带
@Test的方法、Spring 扫描带@Component的类并创建 Bean)。 - 替代部分 XML/配置文件:注解可把配置写在代码旁,减少分散的配置文件,提高可读性和维护性。
基本语法
使用注解
使用注解时,在目标元素前加 @注解类型,必要时在括号内写属性(键值对)。
// 无属性的注解
@Override
public String toString() {
return "Example";
}
// 单属性且名为 value 时可省略键名
@SuppressWarnings("unchecked")
List list = new ArrayList();
// 多属性或非 value 属性需写明键名
@RequestMapping(path = "/user", method = RequestMethod.GET)
public String getUser() {
return "user";
}注解可标注的目标
注解可以标注在:类、接口、枚举、方法、构造方法、字段、参数、局部变量、类型参数 等。具体能标在哪里,由注解定义时的 @Target 元注解决定。
元注解
元注解是「用来修饰注解的注解」,用于定义注解的作用目标、保留策略等。定义自定义注解时经常用到。
| 元注解 | 作用 |
|---|---|
@Target | 指定注解可标注在哪些元素上(TYPE、METHOD、FIELD、PARAMETER 等) |
@Retention | 指定注解保留到何时:SOURCE(仅源码)、CLASS(字节码)、RUNTIME(运行时可用) |
@Documented | 是否将注解包含在 Javadoc 中 |
@Inherited | 子类是否继承父类上的该注解 |
@Repeatable | 同一位置是否可重复使用该注解(JDK 8+) |
示例:读懂 @Override
// 简化理解:@Override 的语义
@Target(ElementType.METHOD) // 只能标在方法上
@Retention(RetentionPolicy.SOURCE) // 仅编译期使用,不会进入字节码
public @interface Override {
}- SOURCE:编译器检查方法是否真的重写了父类/接口方法,检查完即丢弃,不保留到运行时。
- RUNTIME:若要通过反射在运行时读取注解,必须使用
RetentionPolicy.RUNTIME(如 JUnit、Spring 的注解)。
内置注解
JDK 自带常用注解,建议先熟悉其用途。
| 注解 | 作用 |
|---|---|
@Override | 标记方法重写父类或接口方法,编译器会检查签名是否匹配 |
@Deprecated | 标记已过时,调用处会产生编译警告,建议配合 Javadoc 说明替代方案 |
@SuppressWarnings | 抑制指定类型的编译警告(如 "unchecked"、"deprecation") |
@FunctionalInterface | 标记接口为函数式接口(仅一个抽象方法),供 Lambda 使用 |
@SafeVarargs | 声明可变参数用法是安全的,抑制相关警告 |
// @Override:确保重写正确,避免拼写或参数错误
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(name, user.name);
}
// @Deprecated:标记过时方法,调用时会有警告
@Deprecated(since = "2.0", forRemoval = true)
public void oldApi() {
// 建议使用 newApi()
}
// @SuppressWarnings:在已知安全时抑制警告
@SuppressWarnings("unchecked")
public <T> T getFirst(List list) {
return (T) list.get(0);
}提示
@Override 建议在重写父类或实现接口方法时始终显式写上,便于编译器帮你发现签名错误。
自定义注解
定义注解
使用 @interface 定义注解;内部可声明属性(无参方法形式),可指定默认值。注解本质是一种特殊的接口,编译后生成 class 文件。
import java.lang.annotation.*;
// 仅用于方法,运行时保留以便反射读取
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Author {
String name(); // 必填属性
String date() default "2026-01-01"; // 可选,有默认值
}
// 使用
public class MyService {
@Author(name = "Zhang", date = "2026-02-01")
public void doSomething() {
// ...
}
}属性类型限制
注解属性类型可以是:基本类型、String、Class、枚举、注解,以及上述类型的数组。不能是包装类、泛型等复杂类型。
public enum Level {
LOW, MEDIUM, HIGH
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Config {
String name();
Class<?> handler() default Object.class;
Level level() default Level.MEDIUM;
String[] tags() default {};
}使用示例
示例 1:标记与说明(RUNTIME + 反射)
定义一个运行时保留的注解,并在简单「测试运行器」中通过反射只执行带该注解的方法,模拟 JUnit 的 @Test 思路。
import java.lang.annotation.*;
import java.lang.reflect.Method;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RunTest {
String value() default ""; // 可选描述
}
public class Calculator {
@RunTest("加法")
public int add(int a, int b) {
return a + b;
}
public int sub(int a, int b) {
return a - b;
}
}
// 简单「测试运行器」:只执行带 @RunTest 的方法
public static void main(String[] args) throws Exception {
Calculator calc = new Calculator();
for (Method m : Calculator.class.getDeclaredMethods()) {
if (m.isAnnotationPresent(RunTest.class)) {
RunTest rt = m.getAnnotation(RunTest.class);
System.out.println("Run: " + m.getName() + " - " + rt.value());
Object result = m.invoke(calc, 2, 3);
System.out.println(" result: " + result); // 5
}
}
}信息
反射读取注解的典型用法:method.isAnnotationPresent(RunTest.class) 判断是否存在该注解,method.getAnnotation(RunTest.class) 获取注解实例并读取其属性。详见 反射入门。
示例 2:校验约束(结合枚举)
用注解声明「允许的角色」,运行时校验当前用户是否在允许列表中,常用于权限、状态校验等场景。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AllowedRoles {
Role[] value(); // 枚举数组
}
public enum Role {
ADMIN, USER, GUEST
}
public class UserService {
@AllowedRoles({ Role.ADMIN, Role.USER })
public void viewProfile() {
// 仅 ADMIN、USER 可访问
}
}注意事项
- 保留策略:若要在运行时通过反射读取注解,必须使用
@Retention(RetentionPolicy.RUNTIME);仅给编译器或注解处理器用时可使用 SOURCE 或 CLASS。 - 作用目标:
@Target不写则默认可标在任意位置,但建议明确指定,避免误用。 - 属性命名:单属性且命名为
value时,使用时可以省略键名,直接写@MyAnno("xxx")。 - 注解不替代业务逻辑:注解只提供元数据;真正「做事」的是编译器或运行时代码(如反射),不要误以为加注解就会自动生效。
- 慎用 @SuppressWarnings:只在确认无问题时局部抑制警告,避免大面积使用掩盖潜在错误。
注意
自定义注解若要在运行时被读取,除了 RetentionPolicy.RUNTIME,还需确保注解类型本身被 JVM 加载(通常只要使用到标注了该注解的类/方法即可)。
提示
框架中常见注解(如 Spring 的 @Component、@Autowired,JUnit 的 @Test)都是 RUNTIME 保留,由框架在启动或运行测试时通过反射扫描并处理。学完 反射 后,可以自己实现一个简单的「扫描带某注解的类并执行」的小框架,加深理解。
相关链接
- 枚举 — 注解属性常使用枚举类型
- 接口 — 注解本质是特殊接口
- 反射入门 — 运行时读取注解的 API(Class/Method/Field.getAnnotation)
- 单元测试入门 — JUnit 中 @Test、@Before 等注解的用法
- Oracle Java 教程 - 注解