跳到内容

第 1 卷 / 第 13 章 / 第 1 课

13.1 注解,给代码贴标签 ​

学过方法重写、接口和泛型警告后,可以用约 35 分钟完成这篇。你会定义目标与保留策略明确的注解,并判断它究竟由编译器、工具还是运行时程序读取。

1. 注解描述声明,但不改变对象行为 ​

注解也是元数据。它可以贴在类、方法、字段、参数或类型使用位置。标签自身不执行日志、校验或事务,真正的读取者可能是:

  • javac 编译器;
  • 编译期注解处理器;
  • IDE 或静态分析工具;
  • 使用反射或 class 元数据的运行时库。

因此,看到一个注解时先问两件事:“谁读它?”“在什么时候读?”

2. 标准注解 ​

2.1 @Override 检查你的重写承诺 ​

成功示例:

java
class Animal {
    public void speak() {
        System.out.println("Animal speaks");
    }
}

class Dog extends Animal {
    @Override
    public void speak() {
        System.out.println("Woof!");
    }
}

@Override 由编译器读取。它要求方法确实重写父类方法,或实现父接口方法。

把下面代码单独编译,应当失败:

java
class Animal {
    public void speak() {}
}

class Dog extends Animal {
    @Override
    public void bark() {}
}
text
method does not override or implement a method from a supertype

不写 @Override 时,合法的重写仍然会动态分派;问题在于拼写错误或参数错误可能悄悄变成新方法。把注解写上,编译器就能替你检查意图。

2.2 @Deprecated 记录迁移路线 ​

旧 API 仍有人使用,立刻移除会破坏调用方。先标记弃用,并写出替代入口:

java
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:

java
public class Client {
    public static void main(String[] args) {
        new OldTool().oldMethod();
    }
}

使用下面的命令观察弃用和待移除警告:

bash
javac --release 17 -encoding UTF-8 -Xlint:deprecation,removal Client.java OldTool.java

注解让编译器识别弃用,Javadoc 的 @deprecated 标签告诉读者该换成什么。

2.3 @SuppressWarnings 只包住已验证的边界 ​

原始类型和 unchecked cast 可能造成 heap pollution。压制警告前必须先证明边界契约:

java
import java.util.List;

final class LegacyAdapter {
    private LegacyAdapter() {}

    @SuppressWarnings("unchecked")
    static List<String> asNames(Object legacyValue) {
        // 上游协议已经验证:列表中的每个元素都是 String。
        return (List<String>) legacyValue;
    }
}

这次检查还不够:共享列表之后也不能被旧代码写入其他类型。若无法保证整个使用期的约束,逐个检查并复制更直接。下面的方法放在 LegacyAdapter 中,拒绝 null 和非字符串元素,返回不受原列表后续修改影响的列表:

java
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
serialSerializable 类缺少序列版本字段
fallthroughswitch 分支贯穿

键值由编译器决定,不能假定 @SuppressWarnings("all") 在所有编译器上有效。它与 javac 的 -Xlint:all 选项也不是同一套开关。使用具体键值,并在实际编译器上确认只有预期警告被压制。

2.4 @FunctionalInterface 和 @SafeVarargs ​

java
@FunctionalInterface
interface BookFilter {
    boolean accept(String isbn);
}

编译器会拒绝第二个抽象方法,lambda 的目标契约因此更稳固。Object 的 public 方法不计入函数式接口的抽象方法数量。

@SafeVarargs 表示方法作者确认泛型 varargs 的实现不会破坏类型安全。编译器不能完整证明这个承诺。数组泄漏、写入错误元素或不安全转发仍会造成 heap pollution。

3. 自定义注解 ​

3.1 最小声明 ​

java
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 让使用者可以省略该元素。

使用时写:

java
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_COMPONENTrecord 组件

没有 @Target 时,注解可用于除类型参数外的声明位置;这并不开放任意 TYPE_USE 位置。给框架注解指定最小目标,错误会更早出现在编译期。

3.3 @Retention 速查 ​

策略保留到核心反射可读
SOURCE源码阶段否
CLASSclass 文件;默认值否
RUNTIMEJVM 运行时是

@Override 和 @SuppressWarnings 使用 SOURCE,因为编译器处理完就够了。运行时框架需要读取的注解通常使用 RUNTIME。CLASS 元数据仍可被字节码工具读取,不能把“反射不可见”误写成“信息从 class 文件消失”。

3.4 @Inherited 的范围很窄 ​

java
import java.lang.annotation.*;

@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Audited {}

子类调用 getAnnotation(Audited.class) 时可以沿父类查找。以下内容不会因此自动继承:

  • 父类方法、字段或参数上的注解;
  • 父接口上的类级注解;
  • 构造器注解。

这里说的是注解不会复制到重写方法上。如果子类没有重写,getMethod 取得的仍可能是父类那个方法声明,当然能读到该声明自己的注解。框架若要合并接口、方法层级或组合注解,必须实现自己的搜索规则。

3.5 @Repeatable 用容器保存重复值 ​

java
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、枚举、注解和这些类型的一维数组。

java
@interface AuthorInfo {
    String name();
    int version() default 1;
    Class<?> relatedType() default Void.class;
    String[] reviewers() default {};
}

不能使用 List<String>、任意业务对象、二维数组或 null。默认值必须能在编译期确定。

4. 完整实验:目标、默认值、重复值和保留策略 ​

bash
chapter13_annotation_root=$(mktemp -d)
mkdir -p "$chapter13_annotation_root/out"

将下面的代码保存为 $chapter13_annotation_root/AnnotationDemo.java:

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("注解检查通过");
    }
}
bash
javac --release 17 -Xlint:all -Werror \
  -d "$chapter13_annotation_root/out" \
  "$chapter13_annotation_root/AnnotationDemo.java"
java -ea -cp "$chapter13_annotation_root/out" AnnotationDemo
text
目标:METHOD
级别:INFO
角色:[reader, auditor]
SOURCE 运行时可见:false
注解检查通过
bash
rm -rf -- "$chapter13_annotation_root"

这段程序通过反射读取注解。先记住读取 API 的形状,下一篇再拆解 Class 和 Method。

5. 编译失败也是测试 ​

注解贴错位置 ​

java
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@interface MethodOnly {}

@MethodOnly
class WrongTarget {}

预期诊断包含:

text
annotation type not applicable to this kind of declaration

元素类型不合法 ​

java
import java.util.List;

@interface InvalidElement {
    List<String> values();
}

预期诊断包含:

text
invalid type for annotation type element

把失败案例放到单独文件编译。不要为了让教程“全绿”而删除能证明边界的负向实验。

6. 常见卡点 ​

现象先检查
运行时读不到注解是否为 RUNTIME;查询元素是否正确
编译器说目标不适用@Target 是否包含当前位置
重复注解只读到一个是否使用 getAnnotationsByType
子类没有得到注解是否为类注解;是否有 @Inherited;来源是否是接口
默认值无法编译是否使用合法的常量、类字面量、枚举值或注解/数组初始化形式
加了注解却没有行为项目中是否存在真正的读取者或处理器

7. 让读取者证明标签有效 ​

先定义只允许贴在类型上的 @Owner(name, reviewers),保留到运行时,再写一段反射代码读取它。随后定义可重复的 @Permission,验证 getAnnotationsByType 能按声明顺序取回全部权限。

接着给 @Inherited 准备父类、子类和接口三个用例。子类从父类取得类级注解,不代表接口或方法注解也会自动合并;把实际结果写成断言,比记“注解可以继承”更准确。

最后保留两个独立的编译失败文件:一个把 METHOD 注解贴到类上,另一个让注解元素返回 List<String>。修复时分别调整 @Target 和把元素改成合法的一维数组。正例验证读取行为,反例验证声明边界。

查阅 Java 17 的正式规则 ​

下一步:让程序读懂标签 ​

注解已经附着在声明上,下一步是读取元数据并按规则定位对应成员。Java 通过反射 API 提供这组运行时检查能力。

继续学习反射:程序照镜子

Built with VitePress | Software Systems Atlas