Annotations

2021-01-21T10:36:00

Annotation 是一种元数据,它为程序提供不属于自身的信息,对被注解代码的操作不起直接作用。

注解有很多用处,包括:

  • 提供给编译器的信息 —— 编译器可以根据注解检测错误或抑制警告
  • 编译期和部署期处理 —— 工具可以根据注解生成代码、XML 等文件
  • 运行时处理 —— 有些注解可以在运行时被检测到

下文介绍注解可以被用到什么地方,如何使用,JavaSE 提供了哪些预定义注解,怎样在可插拔类型系统中使用类型注解实现强大的类型检查,怎样实现重复注解。

Annotation Basics

The Format of an Annotation

下面是最简单的注解形式:

@Entity

@ 符号告诉编译器,紧跟着的是注解。下面这个注解的名称是 Override

@Override
void mySuperMethod(){}

注解可以包含 elements,它们可以是 namedunnamed,具有 value

@Author(
      name = "Benjamin Franklin",
      date = "3/27/2003"
)
class MyClass {
}

或者

@SupressWarnings(value = "unchecked")
void myMethod(){}

如果仅有一个名为 value 的 element,则可以像下面这样省略 name:

@SupressWarnings("unchecked")
void myMethod(){}

如果一个 element 都没有,还可以像前面 @Override 那样省略圆括号。

也可在相同声明处使用多个注解:


@Author(name = "Jane Doe")
@EBook
class MyClass {
}

如果多个注解类型相同,这种声明称为 repeating annotation


@Author(name = "Jane Doe")
@Author(name = "John Smith")
class MyClass {
}

Java SE 8开始支持重复注解。更多信息见 Repeating Annotations

注解类型可以是 java.langjava.lang.annotation 中定义的一种,上面的 OverrideSuppressWarnings 就是预定义注解。你还可以定义自己的注解类型,就如上面的 AuthorEbook

Where Annotation Can Be Used

注解可被用在声明上:包括类、属性、方法和其他程序元素的声明。当被使用时,按照惯例,每个注解单独占一行。

Java SE 8 开始,注解可以被用在类型的 使用 上,下面是一些例子:

  • 类实例创建表达式

    new @Interned MyObject();
  • 类型转换

    myString = (@NotNull String) str;
  • implements 子句

    class UnmodifiableList<T> implements
          @Readonly List<@Readonly T> {}
  • 抛出异常声明

    void moniterTemperature() throws
          @Critical TemperatureException {}

这种形式的注解称 type annotation。更多信息见 Type Annotations and Pluggable Type Systems

Declaring an Annotation Type

许多注解用来代替注释。假设一个软件开发组习惯在每个类开头通过注释提供重要信息:

public class Generation3List extends Generation2List {
    // Author: John Doe
    // Date: 3/17/2002
    // Current revision: 6
    // Last modified: 4/12/2004
    // By: Jane Doe
    // Reviewers: Alice, Bill, Cindy

    // class code goes here
}

为了使用注解代替它们,先要声明注解类型。下面是声明格式:

@interface ClassPreamble {
    String author();

    String date();

    int currentRevision() default 1;

    String lastModified() default "N/A";

    String lastModifiedBy() default "N/A";

    // Note use of array
    String[] reviewers();
}

注解定义看上去就像接口定义,只是 interface 关键字前多了个 @ (@ = AT) ,它确实是一种接口。

上面的注解定义中包含 annotation type element 声明,看上去就像方法。注意它们可以定义可选的默认值。

完成注解类型定义,就可以使用它了,可以像下面这样为元素赋值:


@ClassPreamble(
        author = "John Doe",
        date = "3/17/2002",
        currentRevision = 6,
        lastModified = "4/12/2004",
        lastModifiedBy = "Jane Doe",
        // Note array notation
        reviewers = {"Alice", "Bob", "Cindy"}
)
public class Generation3List extends Generation2List {
    // class code goes here
}

[tip type="info" title="备注"]
如果想让 @ClassPreamble 包含的信息出现在 Javadoc 生成的文档中,你必须在 @ClassPreamble 的定义上使用 @Documented 注解:
[/tip]

// import this to use @Documented

import java.lang.annotation.*;

@Documented
@interface ClassPreamble {
    // Annotation element definitions
}

Predefined Annotation Types

Java SE API 提供了一组预定义注解,它们有些被编译器使用,有些用于其他注解。

Annotation Types Used by the Java Language

预定义在 java.lang 包下的注解有 @Deprecated, @Override, 和 @SuppressWarnings

@Deprecated 注解表明被标记元素已经过时,不应该继续使用。当代码中出现被 @Deprecated 注解的类,方法或属性时,编译器会生成一个警告。如果一个元素已经过时,它同时应该像下面这样,被 Javadoc 的 @deprecated 标签标注。Javadoc 标签和 Annotataion 注解都使用了 @ 符号,这并不是偶然的:它们在概念上相关。同时,注意 Javadoc 标签以小写 d 开头,而注解是大写 D

class CertainClass {
    // Javadoc comment follows

    /**
     * @deprecated
     * explanation of why it was deprecated
     */
    @Deprecated
    static void deprecatedMethod() {
    }
}

@Override 通知编译器,下面的元素将要覆盖父类中的元素。重写方法将会在 Interfaces and Inheritance 讨论。

// mark method as a superclass method
// that has been overridden
@Override
int overriddenMethod(){}

虽然重写方法时,该注解并非必须,但它可以防止错误。如果一个被标记了 @Override 的方法没有正确覆盖某个父类中的方法,编译器将生成错误。

@SuppressWarnings 告诉编译器抑制某个它本要产生的警告。下例中使用了一个过时方法,编译器本应发出警告,但因为有了上述注解,警告将被抑制。

// use a deprecated method and tell
// complier not to generate a warning
@SuppressWarnings("deprecation")
void useDeprecatedMethod() {
    // deprecation warning
    // - supressed
    objectOne.deprecatedMethod();
}

每个编译器警告属于一个类别。Java 语言规范中列举了两类:deprecatedunchecked。当与引入 泛型 之前的遗留代码交互时,unchecked 警告就会产生。使用下面的语法抑制多种类型警告:

@SuppressWarnings({"unchecked", "deprecation"})

@SafeVarargs 用于方法或构造器时,断言该方法不会对自身的 varargs 产生潜在的不安全操作。此时,与 varargs 使用 相关的 unchecked warnings 将被抑制。

@FunctionalInterface 注解在 Java SE 8 引入,表明要将类型声明为 Java 语言规范定义的函数接口。

Annotation That Apply to Other Annotations

应用于其他注解的注解称为 meta-annotationsjava.lang.annation 中定义了几种元注解类型。

@Retention 指定被标记注解如何储存。

  • RetentionPolicy.SOURCE —— 标记注解仅保留在源码级别,会被编译器忽略。
  • RetentionPolicy.CLASS —— 标记注解在编译时被编译器保留,但会被 JVM 忽略。
  • RetentionPolicy.RUNTIME —— 标记注解被 JVM 保留,所以可以在运行时环境中使用它。

@Documented 表明无论何时使用被它标记的注解,该注解的元素都会通过 Javadoc tool 生成文档。(默认情况下,注解不被包含在 Javadoc 中。)更多信息见 Javadoc tools page

@Target 用于约束被它标记的注解能够应用到哪些 Java 元素上。该注解需要指定以下元素中的一个作为它的值:

  • ElementType.ANNOTATION_TYPE 可以被用在注解上
  • ElementType.CONSTRUCTOR 可以被用在构造器上
  • ElementType.FIELD 可以被用在域或属性上
  • ElementType.LOCAL_VARIABLE 可以被用在局部变量上
  • ElementType.METHOD 可以被用在方法级注解上
  • ElementType.PACKAGE 可以被用在包声明上
  • ElementType.PARAMETER 可以被用在方法的参数上
  • ElementType.TYPE 可以被用到类的任何元素上

@Inherited 表明该注解类型可以从父类继承(默认情况下不可以)。当用户查询一个类的注解类型而该类没有注解时,该类的父类将被查找。这种注解仅用于类型声明。

@RepeatableJava SE 8 被引入,被它标记的注解,在相同声明或类型引用处可以使用多次。更多信息见 Repeating Annotations

Type Annotations and Pluggable Type Systems

Java SE 8 发布前,注解仅能被用到声明上。随着 Java SE 8 的发布,注解也可以用到任何 类型引用 上了。这意味着你可以在任何使用类型的地方使用注解。几个类型使用的例子是 类实例创建表达式castsimplements 子句,和 throws 子句。这种形式的注解称为 type annotation,见 Annotations Basics

类型注解被创建出来支持改进的,确保强类型检查方式的分析。虽然 Java SE 8 没有提供类型检查框架,但它允许你编写(或下载)一个实现了一到多个可插拔模块的框架,这些模块可以和 Java 编译器协同使用。

举例来说,你想保证程序中的某个变量永远不会赋值成 null,从而触发 NullPointerException。你可以通过编写自定义插件来检查它。随后,修改代码,在那个变量上添加注解,表明它永远不被赋值为 null。那个变量的声明可能像下面这样:

@NonNull String str;

当你编译代码时,在命令行中包含 NonNull 模块,如果编译器检测到潜在问题,警告将被打印,提醒你修改代码避免错误。在你改正代码移除所有警告后,程序运行时,该错误将永远不会发生。

你可以使用多个类型检查模块,每个模块检查一种错误。这样,你就可以在 Java 类型系统的基础上进行构建,在需要的时间和地点,增加特定的检查。

审慎地使用类型注解搭配可插拔的类型检查器,你就可以写出健壮的,不容易出错的代码。

许多情况下,你不需要自己编写类型检查模块。第三方已经为你做了。例如,你可能想使用华盛顿大学创建的类型检查框架。它包含一个 NonNull 模块,一个正则表达式框架,和一个互斥锁模块。更多信息见 Checker Framework

Repeating Annotations

有时,你想在声明或类型引用处使用多个类型相同的注解。随着 Java SE 8 的发布,repeating annotations 支持你这样做。

例如,你正在编写一段使用定时器服务的代码,它确保你根据特定时间或计划运行某个方法,类似于 UNIX 的 cron 服务。现在你想为 doPeriodicCleanup 设定一个计时器,让它在每月最后一天,和每周五下午 11 点运行代码。为了启动定时器,创建一个 @Schedule 注解,随后在 doPeriodicCleanup 上运用两次。第一次指定每月最后一天,第二次指定每周五下文 11 点,就像下面的示例代码那样:

@Schedule(dayOfMonth = "last")
@Schedule(dayOfWeek = "Fri", hour = "23")
public void doPeriodicCleanup(){ /* ... */ }

之前的代码将注解应用到了方法上。你可以在任何可以使用注解的地方重复单个注解。例如,你有一个处理未认证访问异常的类。你在它之上分别为经理和其他管理员添加了 @Alert 注解:


@Alert(role = "Manager")
@Alert(role = "Administrator")
public class UnauthorizedAccessException extends SecurityException { 
    // ...
}

出于兼容性考虑,重复注解被储存在编译器自动生成的 container annotation 中。为了让编译器支持该操作,你的代码中需要加上两个声明。

Step 1: Declare a Repeatable Annotation Type

注解类型必须被 @Repeateable 元注解标记。下面的例子定义了一个自定义的可重复注解 @Schedule

import java.lang.annotation.Repeatable;

@Repeatable(Schedules.class)
public @interface Schedule {
    String dayOfMonth() default "first";

    String dayOfWeek() default "Mon";

    int hour() default 12;
}

元注解 @Repeateable 位于圆括号中的值,是容器注解类型,它由 Java 编译器产生,用来存储重复注解。此例中,容器注解类型是 Schedules,所以重复的 @Schedule 注解被存储到 @Schedules 中。

在声明上重复使用相同注解,如果该注解没有被定义为 repeatable,将产生编译时错误。

Step 2: Declare the Containing Annotation Type

容器注解类型必须包含名为 value 的数组元素。数组的成员类型必须是可重复注解类型。Schedules 容器注解类型的声明就像这样:

public @interface Schedules {
    Schedule[] value();
}

Retrieving Annotations

Reflection API 里有几种获取注解的方法。对于返回单个注解的方法,如 AnnotatedElement.getAnnotation(Class\<T>),如果仅存在一个所请求的注解,它的行为不发生改变。如果存在多个所请求的注解,你得到的将是它们的容器注解。在这种情况下,遗留代码仍能工作。Java SE 8 引入了其他方法,如 AnnotatedElement.getAnnotationsByType(Class\<T>).,通过扫描容器注解,一次返回多个注解。阅读 AnnotatedElement 类型规范了解所有可用方法。

Design Considerations

设计注解类型时,你必须考虑这种注解的 cardinality。注解可以被使用零次,一次,或者,如果被标记为 @Repeatable,它可以被使用多次。你可以通过 @Target 元注解限制注解的作用域。例如,创建一个仅可用于方法和属性的可重复注解。细致地设置注解以确保它在使用者手中足够灵活与强大。

Questions and Exercises: Annotations

Questions

1. Question: 下面的接口定义有何错误?

public interface House {
    @Deprecated
    void open();

    void openFrontDoor();

    void openBackDoor();
}

Answer: 应该同时在文档中反映 open 为何过时,替代的方法是哪个。如:

public interface House {
    /**
     * @deprecated use of `open`
     * is discouraged, use
     * `openFrontDoor` or
     * `openBackDoor` instead.
     */
    @Deprecated
    void open();

    void openFrontDoor();

    void openBackDoor();
}

2. Question: 考虑 House 接口的实现类.

public class MyHouse implements House {
    public void open() {
    }

    public void openFrontDoor() {
    }

    public void openBackDoor() {
    }
}

编译这段代码,编译器将产生一个警告,因为 open 已经过时了。怎样做才能消除那个警告。

Answer: 为 open 的实现添加 @Deprecated

public class MyHouse implements House {
    // The documentation is
    // inherited from the interface.
    @Deprecated
    public void open() {
    }

    public void openFrontDoor() {
    }

    public void openBackDoor() {
    }
}

或者,抑制那个警告:

public class MyHouse implements House {
    @SuppressWarnings("deprecation")
    public void open() {
    }

    public void openFrontDoor() {
    }

    public void openBackDoor() {
    }
}

3. Question: 下面的代码能编译通过吗?为什么?

public @interface Meal {
    // ...
}

@Meal("breakfast", mainDish = "cereal")
@Meal("lunch", mainDish = "pizza")
@Meal("dinner", mainDish = "selad")
public void evaluateDiet() { /* ... */ }

Answer: 不能通过编译。JDK 8 之前没有重复注解。JDK 8 之后,如果要重复使用注解,要将 Meal 声明为 @Repeatable,随后定义其容器注解:


@java.lang.annotation.Repreatable(MealContainer.class)
public @interface Meal {
    // ...
}

public @interface MealContainer {
    Meal[] value();
}

Exercises

1. Exercise: 为增强请求定义一个注解,元素是 idsynopsisengineerdate。为 engineer 指定默认值 unassigned,date 指定默认值 unknown

Answer:

/**
 * Describe the Request-for-Enhancement (RFE) annotation type.
 */
public @interface RequestForEnhancement {
    int id();

    String synopsis();

    String engineer() default "[unassigned]";

    String date() default "[unknown]";
}

本文译自 Annotations,译者 LOGI

当前页面是本站的「Baidu MIP」版。发表评论请点击:完整版 »