注解

注解

注释有多种用途,其中包括:

  • 编译器信息——注释可供编译器用于检测错误或抑制警告。
  • 编译时和部署时处理——软件工具可处理注释信息以生成代码、XML文件等。
  • 运行时处理——某些注解可在运行时进行检查。

本节说明注解的使用场景、应用方式、Java平台标准版(Java SE API)中可用的预定义注解类型,以及如何结合可插拔类型系统使用类型注解来编写具有更强类型检查能力的代码,并介绍了如何实现重复注解。

注解的格式

在最简单的形式下,注释看起来如下所示:

@Entity

at符号(@)向编译器表明后续内容为注释。在下例中,注释名称为Override

@Override
void mySuperMethod() { ... }

注释可以包含元素,这些元素可以命名或不命名,并且这些元素具有相应的值:

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

或者

@SuppressWarnings(value = "unchecked")
void myMethod() { ... }

如果只有一个名为 value 的元素,则可以省略名称,例如:

@SuppressWarnings("unchecked")
void myMethod() { ... }

如果注解没有元素,则可以省略括号,如前面的@Override示例所示。

同样可以在同一个声明上使用多个注解:

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

如果注释具有相同类型,则称为重复注释:

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

从 Java SE 8 版本开始支持重复注解。有关详细信息,请参阅“重复注解”部分。
注解类型可以是Java SE API中java.lang或java.lang.annotations包中定义的类型之一。在前面的示例中,Override和SuppressWarnings是预定义的Java注解。也可以定义自己的注解类型。前例中的AuthorEbook注解就是自定义注解类型。

注释的使用场景

注解可应用于声明:类、字段、方法及其他程序元素的声明。当用于声明时,每个注解通常按惯例单独占据一行。
从 Java SE 8 版本开始,注解也可应用于类型使用场景。以下是一些示例:

  • 类实例创建表达式:
new @Interned MyObject();
  • 强制类型转换表达式:
(@NonNull String) obj;
  • 实现接口的声明:
class UnmodifiableList<T> implements
  @Readonly List<@Readonly T> { ... }
  • 抛出异常声明:
void monitorTemperature() throws
  @Critical TemperatureException { ... }

这种注释形式称为类型注解

声明一个注解类型

许多注解取代了代码中的注释。
假设某个软件团队在编写代码时,习惯于在每个类的本体部分开头添加注释,以提供重要信息:

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符号(@)(@代表AT,即注解类型)。注解类型属于接口的一种形式,将在后续章节中详细说明。目前您无需理解接口的概念。

先前注释定义的主体包含注释类型元素声明,其外观与方法非常相似。请注意,它们可以定义可选的默认值。

定义注释类型后,即可使用该类型的注释,并填入相应值,如下所示:

@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

}

注意:要使 @ClassPreamble 中的信息出现在Javadoc生成的文档中,必须使用 @Documented 注解标注 @ClassPreamble 的定义:

// import this to use @Documented
import java.lang.annotation.*;

@Documented
@interface ClassPreamble {

   // Annotation element definitions
   
}

预定义注解类型

Java SE API 中预定义了一组注解类型。其中部分注解类型由 Java 编译器使用,另一些则适用于其他注解。

Java语言使用的注解类型

在 java.lang 中定义的预定义注解类型包括 @Deprecated@Override@SuppressWarnings

@Deprecated

@Deprecated 注解表示标记的元素已弃用,不应再使用。当程序使用带有 @Deprecated 注解的方法、类或字段时,编译器会生成警告。元素被标记为弃用后,还应使用 Javadoc 的 @deprecated 标签进行文档说明,如下例所示。Javadoc 注释与注解中均使用 @ 符号并非偶然:二者在概念上存在关联。另请注意:Javadoc 标签以小写字母 d 开头,而注解以大写字母 D 开头。

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

请注意,从 Java SE 9 开始,@Deprecated 注解新增了 forRemoval 属性。该属性用于标识被注解的元素是否会在未来版本中被移除,其默认值为 false

@Override

@Override 注解告知编译器该元素旨在覆盖父类中声明的元素。关于方法覆盖的讨论将在“接口与继承”章节中展开。

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

虽然重写方法时并非必须使用此注解,但它有助于避免错误。若标记为@Override的方法未能正确重写其父类中的某个方法,编译器将生成错误。

@SuppressWarnings

@SuppressWarnings 注解指示编译器抑制其本应生成的特定警告。在下面的示例中,使用了一个已弃用的方法,编译器通常会生成警告。但在这种情况下,该注解会导致警告被抑制。

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

每个编译器警告都属于某个类别。Java语言规范列出了两类警告:弃用警告和未检查警告。在与泛型出现之前编写的旧代码进行交互时,可能会出现未检查警告。要抑制多类警告,请使用以下语法:

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

@SafeVarargs

@SafeVarargs注解应用于方法或构造函数时,它会断言代码不会对其可变参数参数执行潜在不安全的操作。使用此注解类型时,与可变参数使用相关的未检查警告将被抑制。

@FunctionalInterface

@FunctionalInterface 注解在 Java SE 8 中引入,用于标识类型声明旨在作为函数式接口,其定义遵循 Java 语言规范。

适用于其他注解的注解类型

适用于其他注解的注解称为元注解。在 java.lang.annotation 中定义了多种元注解类型。

@Retention

@Retention 注解指定标记注解的存储方式:

  • RetentionPolicy.SOURCE - 标记注释仅保留在源代码层级,编译器会忽略它。
  • RetentionPolicy.CLASS - 标记注释在编译时会被编译器保留,但在Java虚拟机(JVM)中会被忽略。
  • RetentionPolicy.RUNTIME - 标记注释由JVM保留,以便运行时环境能够使用它。

@Documented

@Documented 注解表示,每当使用指定注解时,应使用 Javadoc 工具对这些元素进行文档化。(默认情况下,注解不会包含在 Javadoc 中。)有关更多信息,请参阅 Javadoc 工具页面。

@Target

@Target 注解用于标记另一个注解,以限制该注解可应用于何种类型的Java元素。目标注解将其值限定为以下元素类型之一:

@Inherited

@Inherited 注解表示该注解类型可从父类继承(默认情况下并非如此)。当用户查询注解类型时,若该类未声明此类型注解,则会查询该类的父类以获取注解类型。此注解仅适用于类声明。

@Repeatable

@Repeatable 注解(在 Java SE 8 中引入)表示标记的注解可多次应用于同一声明或类型使用。更多信息请参阅“重复注解”章节。

类型注解与可插拔类型系统

在 Java SE 8 发布之前,注解只能应用于声明。自 Java SE 8 发布以来,注解也可应用于任何类型的使用场景。这意味着注解可在使用类型的任何位置使用。类型使用的场景包括:类实例创建表达式(new)、强制类型转换、implements子句和throws子句等。此类注解称为类型注解,其具体示例详见”注解基础”章节。

类型注解的创建旨在支持对Java程序进行更深入的分析,从而实现更严格的类型检查。Java SE 8版本虽未提供内置的类型检查框架,但允许开发者编写(或下载)类型检查框架——该框架以一个或多个可插拔模块的形式实现,可与Java编译器协同工作。

例如,你希望确保程序中的某个特定变量永远不会被赋值为空,从而避免触发空指针异常。你可以编写自定义插件来检查此情况。随后,你需要修改代码,为该特定变量添加注解,表明它永远不会被赋值为空。变量声明可能如下所示:

@NonNull String str;

当您在命令行中包含 NonNull 模块编译代码时,若编译器检测到潜在问题,会输出警告提示,以便您修改代码避免错误。在修正代码消除所有警告后,程序运行时就不会出现此特定错误。

您可以使用多个类型检查模块,每个模块负责检查不同类型的错误。通过这种方式,您可以在Java类型系统的基础上进行扩展,在需要时添加特定的检查。

通过合理使用类型注解和可插拔类型检查器的支持,您可以编写更强大且不易出错的代码。

在许多情况下,您无需自行编写类型检查模块。已有第三方为您完成了这项工作。例如,您可以利用华盛顿大学创建的 Checker Framework 。该框架包含NonNull模块、正则表达式模块以及互斥锁模块。更多信息请参阅 Checker Framework 文档。

重复注解

在某些情况下,您可能希望将相同的注解应用于声明或类型使用。从 Java SE 8 版本开始,重复注解功能使您能够实现这一目标。
例如,你正在编写代码来使用定时器服务,该服务允许你在指定时间或按特定计划运行方法,类似于UNIX的cron服务。现在你需要设置定时器,在每月最后一天及每周五晚上11点运行 doPeriodicCleanup() 方法。为此需创建 @Schedule 注解并将其应用于该方法两次: 首次使用指定每月最后一天,第二次指定周五晚上11点,如下代码示例所示:

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

前面的示例将注解应用于方法。您可以在任何需要使用标准注解的位置重复使用注解。例如,您有一个用于处理未授权访问异常的类。您可以为该类添加两个注解:一个@Alert注解用于管理员,另一个用于系统管理员:

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

出于兼容性考虑,重复注解会被存储在Java编译器自动生成的容器注解中。为使编译器完成此操作,您的代码中需要包含两处声明。

第一步:声明一个可重复的注解类型

注解类型必须标记为 @Repeatable 元注解。以下示例定义了一个自定义的 @Schedule 可重复注解类型:

import java.lang.annotation.Repeatable;

@Repeatable(Schedules.class)
public @interface Schedule {
  String dayOfMonth() default "first";
  String dayOfWeek() default "Mon";
  int hour() default 12;
}

括号中的@Repeatable元注解的值,即为Java编译器生成的用于存储重复注解的容器注解类型。在此示例中,包含注解的类型为@Schedules,因此重复的@Schedule注解将存储于@Schedules注解中。

若未事先声明声明为可重复,而直接为其添加相同注解,将导致编译时错误。

第二步:声明容器注解类型

包含注解类型必须具有数组类型的值元素。该数组类型的组成部分类型必须是可重复注解类型。@Schedules包含注解类型的声明如下:

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

检索注释

反射API中提供了多种用于检索注解的方法。返回单个注解的方法(如AnnotatedElement.getAnnotation(Class))的行为保持不变:当请求类型仅存在一个注解时,它们仍仅返回该注解。若存在多个请求类型的注解,可先获取其容器注解来获取所有注解。通过这种方式,旧代码仍可正常运行。Java SE 8中引入了其他方法,可遍历容器注解一次性返回多个注解,例如AnnotatedElement.getAnnotationsByType(Class)。有关所有可用方法的详细信息,请参阅AnnotatedElement类的规范说明。

设计考虑因素

设计注解类型时,必须考虑该类型注解的基数性。现在注解可以零次使用、使用一次,或者(如果注解类型标记为@Repeatable)使用多次。还可通过@Target元注解限制注解类型的使用范围。例如,可创建仅限于方法和字段使用的可重复注解类型。精心设计注解类型至关重要,这能确保使用该注解的程序员获得最大程度的灵活性与功能性。