13.1 注解,给代码贴标签
学过方法重写、接口和泛型警告后,可以用约 35 分钟完成这篇。你会定义目标与保留策略明确的注解,并判断它究竟由编译器、工具还是运行时程序读取。
1. 注解描述声明,但不改变对象行为
注解也是元数据。它可以贴在类、方法、字段、参数或类型使用位置。标签自身不执行日志、校验或事务,真正的读取者可能是:
javac编译器;- 编译期注解处理器;
- IDE 或静态分析工具;
- 使用反射或 class 元数据的运行时库。
因此,看到一个注解时先问两件事:“谁读它?”“在什么时候读?”
2. 标准注解
2.1 @Override 检查你的重写承诺
成功示例:
class Animal {
public void speak() {
System.out.println("Animal speaks");
}
}
class Dog extends Animal {
@Override
public void speak() {
System.out.println("Woof!");
}
}@Override 由编译器读取。它要求方法确实重写父类方法,或实现父接口方法。
把下面代码单独编译,应当失败:
class Animal {
public void speak() {}
}
class Dog extends Animal {
@Override
public void bark() {}
}method does not override or implement a method from a supertype不写 @Override 时,合法的重写仍然会动态分派;问题在于拼写错误或参数错误可能悄悄变成新方法。把注解写上,编译器就能替你检查意图。
2.2 @Deprecated 记录迁移路线
旧 API 仍有人使用,立刻移除会破坏调用方。先标记弃用,并写出替代入口:
final class OldTool {
/**
* @deprecated 从 2.0 起使用 {@link #newMethod()}。
*/
@Deprecated(since = "2.0", forRemoval = true)
public void oldMethod() {
System.out.println("旧方法");
}
public void newMethod() {
System.out.println("新方法");
}
}since 与 forRemoval 需要 Java 9+。这里的 "2.0" 是示例库版本;forRemoval = true 表示计划在未来移除,没有承诺具体日期,也不会阻止调用。把上例存为 OldTool.java,再增加 Client.java:
public class Client {
public static void main(String[] args) {
new OldTool().oldMethod();
}
}使用下面的命令观察弃用和待移除警告:
javac --release 17 -encoding UTF-8 -Xlint:deprecation,removal Client.java OldTool.java注解让编译器识别弃用,Javadoc 的 @deprecated 标签告诉读者该换成什么。
2.3 @SuppressWarnings 只包住已验证的边界
原始类型和 unchecked cast 可能造成 heap pollution。压制警告前必须先证明边界契约:
import java.util.List;
final class LegacyAdapter {
private LegacyAdapter() {}
@SuppressWarnings("unchecked")
static List<String> asNames(Object legacyValue) {
// 上游协议已经验证:列表中的每个元素都是 String。
return (List<String>) legacyValue;
}
}这次检查还不够:共享列表之后也不能被旧代码写入其他类型。若无法保证整个使用期的约束,逐个检查并复制更直接。下面的方法放在 LegacyAdapter 中,拒绝 null 和非字符串元素,返回不受原列表后续修改影响的列表:
static List<String> checkedNames(Object value) {
if (!(value instanceof List<?> source)) {
throw new IllegalArgumentException("Expected a list");
}
List<String> result = new java.util.ArrayList<>();
for (Object element : source) {
if (!(element instanceof String name)) {
throw new IllegalArgumentException("Expected a string element");
}
result.add(name);
}
return List.copyOf(result);
}只有旧 API 无法修改、拷贝又不合适且契约确实可靠时,才保留窄范围压制。
常见键值:
| 值 | 常见来源 |
|---|---|
unchecked | 未检查转换或泛型 varargs |
rawtypes | 使用原始泛型类型 |
deprecation | 调用弃用 API |
serial | Serializable 类缺少序列版本字段 |
fallthrough | switch 分支贯穿 |
键值由编译器决定,不能假定 @SuppressWarnings("all") 在所有编译器上有效。它与 javac 的 -Xlint:all 选项也不是同一套开关。使用具体键值,并在实际编译器上确认只有预期警告被压制。
2.4 @FunctionalInterface 和 @SafeVarargs
@FunctionalInterface
interface BookFilter {
boolean accept(String isbn);
}编译器会拒绝第二个抽象方法,lambda 的目标契约因此更稳固。Object 的 public 方法不计入函数式接口的抽象方法数量。
@SafeVarargs 表示方法作者确认泛型 varargs 的实现不会破坏类型安全。编译器不能完整证明这个承诺。数组泄漏、写入错误元素或不安全转发仍会造成 heap pollution。
3. 自定义注解
3.1 最小声明
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 Loggable {
String level() default "INFO";
boolean includeArgs() default false;
}语法拆解:
@interface声明注解类型;@Target(METHOD)限定可标记的方法声明;@Retention(RUNTIME)让核心反射在运行时读取;level()是必有返回类型的注解元素;default让使用者可以省略该元素。
使用时写:
final class OrderService {
@Loggable(level = "DEBUG")
public void placeOrder(String userId, String itemId) {
System.out.println("下单中:" + userId + "/" + itemId);
}
@Loggable
public void queryOrder(String orderId) {
System.out.println("查询订单:" + orderId);
}
}这段代码没有自动日志。下一篇会写读取者。
3.2 @Target 速查
ElementType | 位置 |
|---|---|
TYPE | 类、接口、枚举、record、注解类型 |
METHOD | 方法 |
FIELD | 字段、枚举常量 |
PARAMETER | 方法或构造器参数 |
CONSTRUCTOR | 构造器 |
ANNOTATION_TYPE | 注解类型 |
TYPE_PARAMETER | 泛型类型参数声明,如 <@Marker T> |
TYPE_USE | 类型出现的位置,如 List<@NonNull String> |
RECORD_COMPONENT | record 组件 |
没有 @Target 时,注解可用于除类型参数外的声明位置;这并不开放任意 TYPE_USE 位置。给框架注解指定最小目标,错误会更早出现在编译期。
3.3 @Retention 速查
| 策略 | 保留到 | 核心反射可读 |
|---|---|---|
SOURCE | 源码阶段 | 否 |
CLASS | class 文件;默认值 | 否 |
RUNTIME | JVM 运行时 | 是 |
@Override 和 @SuppressWarnings 使用 SOURCE,因为编译器处理完就够了。运行时框架需要读取的注解通常使用 RUNTIME。CLASS 元数据仍可被字节码工具读取,不能把“反射不可见”误写成“信息从 class 文件消失”。
3.4 @Inherited 的范围很窄
import java.lang.annotation.*;
@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Audited {}子类调用 getAnnotation(Audited.class) 时可以沿父类查找。以下内容不会因此自动继承:
- 父类方法、字段或参数上的注解;
- 父接口上的类级注解;
- 构造器注解。
这里说的是注解不会复制到重写方法上。如果子类没有重写,getMethod 取得的仍可能是父类那个方法声明,当然能读到该声明自己的注解。框架若要合并接口、方法层级或组合注解,必须实现自己的搜索规则。
3.5 @Repeatable 用容器保存重复值
import java.lang.annotation.*;
@Repeatable(Roles.class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Role {
String value();
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Roles {
Role[] value();
}
@Role("reader")
@Role("auditor")
final class CatalogService {}读取重复注解时使用 getAnnotationsByType(Role.class),不要只调用 getAnnotation(Role.class)。
3.6 元素类型限制
合法元素类型包括基本类型、String、Class、枚举、注解和这些类型的一维数组。
@interface AuthorInfo {
String name();
int version() default 1;
Class<?> relatedType() default Void.class;
String[] reviewers() default {};
}不能使用 List<String>、任意业务对象、二维数组或 null。默认值必须能在编译期确定。
4. 完整实验:目标、默认值、重复值和保留策略
chapter13_annotation_root=$(mktemp -d)
mkdir -p "$chapter13_annotation_root/out"将下面的代码保存为 $chapter13_annotation_root/AnnotationDemo.java:
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.util.Arrays;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface Loggable {
String level() default "INFO";
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Roles.class)
@interface Role {
String value();
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Roles {
Role[] value();
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
@interface SourceMarker {}
@Role("reader")
@Role("auditor")
final class CatalogService {
@Loggable
@SourceMarker
public void search() {}
}
public class AnnotationDemo {
public static void main(String[] args) throws NoSuchMethodException {
Target target = Loggable.class.getAnnotation(Target.class);
Method method = CatalogService.class.getMethod("search");
Loggable loggable = method.getAnnotation(Loggable.class);
String[] roles = Arrays.stream(CatalogService.class.getAnnotationsByType(Role.class))
.map(Role::value)
.toArray(String[]::new);
boolean sourceVisible = method.isAnnotationPresent(SourceMarker.class);
System.out.println("目标:" + target.value()[0]);
System.out.println("级别:" + loggable.level());
System.out.println("角色:" + Arrays.toString(roles));
System.out.println("SOURCE 运行时可见:" + sourceVisible);
if (!Arrays.equals(target.value(), new ElementType[]{ElementType.METHOD})
|| !"INFO".equals(loggable.level())) {
throw new AssertionError("Target or default value mismatch");
}
if (!Arrays.equals(roles, new String[]{"reader", "auditor"})) {
throw new AssertionError("重复注解顺序错误");
}
if (sourceVisible) {
throw new AssertionError("SOURCE 注解不应在运行时可见");
}
System.out.println("注解检查通过");
}
}javac --release 17 -Xlint:all -Werror \
-d "$chapter13_annotation_root/out" \
"$chapter13_annotation_root/AnnotationDemo.java"
java -ea -cp "$chapter13_annotation_root/out" AnnotationDemo目标:METHOD
级别:INFO
角色:[reader, auditor]
SOURCE 运行时可见:false
注解检查通过rm -rf -- "$chapter13_annotation_root"这段程序通过反射读取注解。先记住读取 API 的形状,下一篇再拆解 Class 和 Method。
5. 编译失败也是测试
注解贴错位置
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@interface MethodOnly {}
@MethodOnly
class WrongTarget {}预期诊断包含:
annotation type not applicable to this kind of declaration元素类型不合法
import java.util.List;
@interface InvalidElement {
List<String> values();
}预期诊断包含:
invalid type for annotation type element把失败案例放到单独文件编译。不要为了让教程“全绿”而删除能证明边界的负向实验。
6. 常见卡点
| 现象 | 先检查 |
|---|---|
| 运行时读不到注解 | 是否为 RUNTIME;查询元素是否正确 |
| 编译器说目标不适用 | @Target 是否包含当前位置 |
| 重复注解只读到一个 | 是否使用 getAnnotationsByType |
| 子类没有得到注解 | 是否为类注解;是否有 @Inherited;来源是否是接口 |
| 默认值无法编译 | 是否使用合法的常量、类字面量、枚举值或注解/数组初始化形式 |
| 加了注解却没有行为 | 项目中是否存在真正的读取者或处理器 |
7. 让读取者证明标签有效
先定义只允许贴在类型上的 @Owner(name, reviewers),保留到运行时,再写一段反射代码读取它。随后定义可重复的 @Permission,验证 getAnnotationsByType 能按声明顺序取回全部权限。
接着给 @Inherited 准备父类、子类和接口三个用例。子类从父类取得类级注解,不代表接口或方法注解也会自动合并;把实际结果写成断言,比记“注解可以继承”更准确。
最后保留两个独立的编译失败文件:一个把 METHOD 注解贴到类上,另一个让注解元素返回 List<String>。修复时分别调整 @Target 和把元素改成合法的一维数组。正例验证读取行为,反例验证声明边界。
查阅 Java 17 的正式规则
- JLS 9.6:注解接口:元素类型、默认值和元注解规则。
- JLS 9.7:注解:正常、标记、单元素与重复注解的语法和适用性。
java.lang.annotation:Target、Retention、Inherited与Repeatable。AnnotatedElement:运行时读取直接、继承和重复注解的契约。
下一步:让程序读懂标签
注解已经附着在声明上,下一步是读取元数据并按规则定位对应成员。Java 通过反射 API 提供这组运行时检查能力。