2

注解

注解(一种元数据形式)提供有关不属于程序本身的程序的数据,注解对它们注解的代码的操作没有直接影响。

注解有许多用途,其中包括:

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

本课程介绍了可以使用注解的位置,以及如何应用注解,Java平台标准版(Java SE API)中提供了哪些预定义注解类型,类型注解如何与可插拔类型系统结合使用来编写具有更强类型检查的代码,以及如何实现重复注解。

注解基础知识

注解的格式

在最简单的形式中,注解如下所示:

@Entity

符号字符(@)向编译器指示后面的内容是注解,在以下示例中,注解的名称为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.langjava.lang.annotation包中定义的类型之一,在前面的示例中,OverrideSuppressWarnings是预定义的Java注解,也可以定义自己的注解类型,上一个示例中的AuthorEbook注解是自定义注解类型。

可以使用注解的位置

注解可以应用于声明:类、字段、方法和其他程序元素的声明,当在声明中使用时,按照惯例,每个注解通常出现在它自己的行上。

从Java SE 8发行版开始,注解也可以应用于类型的使用,这里有些例子:

  • 类实例创建表达式:

    new @Interned MyObject();
  • 输入:

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

    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注释和注解中使用at符号(@)并非巧合:它们在概念上是相关的,另请注意,Javadoc标记以小写d开头,注解以大写D开头。

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

@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语言规范列出了两个类别:deprecationunchecked,当在泛型出现之前编写的遗留代码接口时,可能会发生unchecked警告,要禁止多种类别的警告,请使用以下语法:

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

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

@FunctionalInterface:Java SE 8中引入的@FunctionalInterface注解,表示类型声明旨在成为Java语言规范定义的功能接口。

应用于其他注解的注解

应用于其他注解的注解称为元注解,java.lang.annotation中定义了几种元注解类型。

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

  • RetentionPolicy.SOURCE — 标记的注解仅保留在源级别中,并被编译器忽略。
  • RetentionPolicy.CLASS — 标记的注解在编译时由编译器保留,但Java虚拟机(JVM)会忽略。
  • RetentionPolicy.RUNTIME — 标记的注解由JVM保留,因此运行时环境可以使用它。

@Documented@Documented注解表明,无论何时使用指定的注解,都应使用Javadoc工具记录这些元素(默认情况下,注解不包含在Javadoc中),有关更多信息,请参阅Javadoc工具页面

@Target:@Target注解标记另一个注解,以限制可以应用注解的Java元素类型,目标注解指定以下元素类型之一作为其值:

  • ElementType.ANNOTATION_TYPE可以应用于注解类型。
  • ElementType.CONSTRUCTOR可以应用于构造函数。
  • ElementType.FIELD可以应用于字段或属性。
  • ElementType.LOCAL_VARIABLE可以应用于局部变量。
  • ElementType.METHOD可以应用于方法级注解。
  • ElementType.PACKAGE可以应用于包声明。
  • ElementType.PARAMETER可以应用于方法的参数。
  • ElementType.TYPE可以应用于类的任何元素。

@Inherited@Inherited注解表明注解类型可以从超类继承(默认情况下不是这样),当用户查询注解类型并且该类没有此类型的注解时,将查询类的超类以获取注解类型,此注解仅适用于类声明。

@Repeatable:Java SE 8中引入的@Repeatable注解表明标记的注解可以多次应用于相同的声明或类型使用,有关更多信息,请参阅重复注解。

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

在Java SE 8发行版之前,注解只能应用于声明,从Java SE 8发行版开始,注解也可以应用于任何类型的使用,这意味着可以在任何使用类型的地方使用注解。使用类型的一些示例包括类实例创建表达式(new)、类型转换、implements子句和throws子句,这种注解形式称为类型注解,注解基础知识中提供了几个示例。

创建类型注解是为了支持改进的Java程序分析,以确保更强的类型检查,Java SE 8版本不提供类型检查框架,但它允许你编写(或下载)类型检查框架,该框架实现为与Java编译器结合使用的一个或多个可插拔模块。

例如,你希望确保程序中的特定变量永远不会分配给null,你想避免触发NullPointerException,你可以编写自定义插件来检查此问题,然后,你将修改代码以注解该特定变量,表明它永远不会被赋值为null,变量声明可能如下所示:

@NonNull String str;

当你编译代码(包括命令行中的NonNull模块)时,编译器会在检测到潜在问题时输出警告,允许你修改代码以避免错误,在更正代码以移除所有警告后,程序运行时不会发生此特定错误。

你可以使用多个类型检查模块,其中每个模块检查不同类型的错误,通过这种方式,你可以在Java类型系统的基础上构建,在你希望的时间和位置添加特定的检查。

通过明智地使用类型注解和可插拔类型检查器,你可以编写更强大且更不容易出错的代码。

在许多情况下,你不必编写自己的类型检查模块,有第三方为你完成了这项工作,例如,你可能希望利用华盛顿大学创建的Checker Framework,该框架包括NonNull模块、正则表达式模块和互斥锁模块,有关更多信息,请参阅Checker Framework

重复注解

在某些情况下,你希望将相同的注解应用于声明或类型用途,从Java SE 8发行版开始,重复注解使你可以执行此操作。

例如,你正在编写代码以使用计时器服务,该服务使你能够在给定时间或某个计划上运行方法,类似于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 { ... }

出于兼容性原因,重复注解存储在由Java编译器自动生成的容器注解中,为了使编译器执行此操作,代码中需要两个声明。

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

注解类型必须使用@Repeatable元注解进行标记,以下示例定义自定义@Schedule可重复注解类型:

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

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

将相同的注解应用于声明而不首先声明它是可重复的,这会导致编译时错误。

第2步:声明包含注解类型

包含注解类型必须具有带数组类型的value元素,数组类型的组件类型必须是可重复的注解类型,包含注解类型的Schedules的声明如下:

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

检索注解

Reflection API中有几种可用于检索注解的方法,返回单个注解的方法(例如AnnotatedElement.getAnnotation(Class<T>))的行为未更改,因为如果存在所请求类型的一个注解,它们仅返回单个注解,如果存在多个所请求类型的注解,则可以通过首先获取其容器注解来获取它们,通过这种方式,遗留代码继续工作。Java SE 8中引入了其他方法,它们扫描容器注解以一次返回多个注解,例如AnnotatedElement.getAnnotationsByType(Class<T>),有关所有可用方法的信息,请参阅AnnotatedElement类规范。

设计注意事项

设计注解类型时,必须考虑该类型注解的基数,现在可以使用注解零次、一次,或者,如果注解的类型标记为@Repeatable,则不止一次,通过使用@Target元注解,还可以限制注解类型的使用位置。例如,你可以创建只能在方法和字段上使用的可重复注解类型,仔细设计注解类型非常重要,以确保使用注解的程序员发现它尽可能灵活和强大。


上一篇:枚举类型
下一篇:接口

博弈
2.5k 声望1.5k 粉丝

态度决定一切