注解(Annotations)

注解为程序提供元数据(metadata).元数据又称中介数据、中继数据,为描述数据的数据(data about data),主要是描述数据属性(property)的信息。它不会影响程序的编译方式,也不会影响最终的编译结果。

注解有如下的使用场景:

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

注解的格式

注解的格式的通常拥有键/值对,其键就是方法名。
格式如下:

  1. @Entity

符号@告诉编译器这是个注解。

注解可以包含有名字或者没有名字的元素(elements),如:

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

或者

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

当只有一个元素名字是 value 时,该名字可以省略,如:

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

若注解没有元素,则连圆括号都可以省略。

同一个声明可以用多个注解:

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

若注解包含相同的类型,则被称为重复注解(repeating annotation):

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

重复注解是 Java SE 8 里面支持的。

注解使用的地方

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

Java SE8 开始,注解也可以应用于类型使用(type use),称为类型注解(type annotation)。 这里有些例子:

  • 类实例创建表达式
  1. new @Interned MyObject()
  • 类型投射
  1. myString = (@NonNull String) str;
  • 实现条款
  1. class UnmodifiableList<T> implements
  2. @Readonly List<@Readonly T> { ... }
  • 抛出异常声明
  1. void monitorTemperature() throws
  2. @Critical TemperatureException { ... }

声明一个注解类型

许多注解取代了本来已经在代码中的注释。

假设传统的软件组在每个类的类体的开始,使用注释提供了重要的信息:

  1. public class Generation3List extends Generation2List {
  2. // Author: John Doe
  3. // Date: 3/17/2002
  4. // Current revision: 6
  5. // Last modified: 4/12/2004
  6. // By: Jane Doe
  7. // Reviewers: Alice, Bill, Cindy
  8. // class code goes here
  9. }

使用注解提供一样的元数据,首先要声明一个注解类型,语法是:

  1. @interface ClassPreamble {
  2. String author();
  3. String date();
  4. int currentRevision() default 1;
  5. String lastModified() default "N/A";
  6. String lastModifiedBy() default "N/A";
  7. // Note use of array
  8. String[] reviewers();
  9. }

注解的声明,就像在 interface 声明前面添加一个@字符(@是AT,即 Annotation Type)。注解类型,其实是接口的一种特殊形式,后面会讲到。就目前而言,你不需要了解。

注解的声明的正文,包括注解元素的声明,看起来很像方法。注意,这里可以定义可选的默认值。

一旦注解定义好了,就可以在使用注解时,填充注解的值,就像这样:

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

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

  1. // import this to use @Documented
  2. import java.lang.annotation.*;
  3. @Documented
  4. @interface ClassPreamble {
  5. // Annotation element definitions
  6. }

预定义注解的类型

有这么几种注解类型预定义在 Java SE API 了。一些注解类型是供 Java 编译器使用,一些是供其他注解使用。

Java 语言使用的注解

定义在 java.lang 中的是 @Deprecated, @Override, 和 @SuppressWarnings

@Deprecated注解指示,标识的元素是废弃的(deprecated),不应该再使用。编译器会在任何使用到@Deprecated的类,方法,字段的程序时产生警告。当元素是废弃的,它也应该使用 Javadoc 的 @deprecated 标识文档化,如下面的例子。两个Javadoc 注释和注解中的“@”符号的使用不是巧合 - 它们是相关的概念上。另外,请注意Javadoc标记开始用小写字母“d”和注解开始以大写字母“D”。

  1. // Javadoc comment follows
  2. /**
  3. * @deprecated
  4. * explanation of why it was deprecated
  5. */
  6. @Deprecated
  7. static void deprecatedMethod() { }
  8. }

@Override注解通知编译器,覆盖父类声明的元素。

  1. // mark method as a superclass method
  2. // that has been overridden
  3. @Override
  4. int overriddenMethod() { }

虽然不要求在覆盖方法时,必须使用注解,但是它可以避免错误。如果一个方法标记为@Override,但是无法正确覆盖父类的任何方法,编译器会产生错误。

@SuppressWarnings告诉编译器,抑制正常情况下会产生的特定的警告。下面的例子,一个废弃的方法被使用,编译器正常会产生警告,而这个情况下,这个注解导致警告会被抑制。

  1. // use a deprecated method and tell
  2. // compiler not to generate a warning
  3. @SuppressWarnings("deprecation")
  4. void useDeprecatedMethod() {
  5. // deprecation warning
  6. // - suppressed
  7. objectOne.deprecatedMethod();
  8. }

每个编译器的警告属于一个类别。Java 语言规范有两个类别:”deprecation” 和”unchecked”。”unchecked” 会在使用以前的写的泛型的遗留代码进行交互时,产生警告。抑制更多类别的警告,使用下面的语法:

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

@SafeVarargs注解,当应用于方法或构造,断言代码不对其可变参数(varargs)的参数进行潜在的不安全操作。当使用这个注释类型时,与可变参数相关未检查警告被抑制。

@FunctionalInterface是在 Java SE 8 中引入,由 Java 语言规范定义的那样,表示该类型声明意在成为功能性的接口。

注解应用于其他注解

注解应用于其他注解称为元注解( meta-annotations)。java.lang.annotation 中定义了多种元注解。

@Retention 注解指定了标记的注解如何存储:

  • RetentionPolicy.SOURCE - 该标记注解只保留在源码级,在编译阶段丢弃。这些注解在编译结束之后就不再有任何意义,所以它们不会写入字节码。@Override@SuppressWarnings都属于这类注解。
  • RetentionPolicy.CLASS - 该标记注释是由编译器在编译时保留, 在类加载的时候丢弃。在字节码文件的处理中有用。注解默认使用这种方式。
  • RetentionPolicy.RUNTIME - 该标记注解由JVM保留,因此可以使用在运行时环境。因此可以使用反射机制读取该注解的信息。我们自定义的注解通常使用这种方式。

@Documented注释表明,只要指定哪些元素应该使用 Javadoc 工具。 (默认情况下,注解不包括在 Javadoc 中。)有关详细信息,请参阅的 Javadoc 工具页面

@Target 用于标记其他注解,限制什么样的 Java 元素的注解可以应用到。@Target 注解指定以下元素类型作为其值之一:

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

@Inherited 指示注释类型可以从超类继承。(默认不是 true)。当用户查询注释类型,类没有这种类型注释,此时从这个类的父类中查询注释类型。这个注释只适用于类的声明。

@Repeatable注解,在 Java SE 8 中引入的,表示该标记的注解可以多次应用到同一声明或类型使用。欲了解更多信息,请参阅重复注解。

类型注解以及可拔插的类型系统

Java SE8 之前,注解只能用于声明,从 Java SE8 开始,注解也可以应用于类型使用(type use),称为类型注解(type annotation)。意味着,注解可以使用在任何使用的类型。

类型注解为 Java 程序提供了更强的类型检查分析。Java SE 8 版本不提供类​​型检查的框架,但它可以让你自己写(或下载)类型检查框架,该框架实现了与 Java 编译器一起使用的一个或多个可插拔模块。

例如,要确保在你的程序中一个特定变量从未被分配到 null ,从而避免引发NullPointerException 异常。您可以编写自定义插件来检查这一点。然后,您可以修改代码以注明这个特定变量,以表明它是永远不会分配给 null。变量声明可能是这样的:

  1. @NonNull String str;

当您编译代码,包括在命令行中的 NonNull 模块,如果它检测到潜在的问题,编译器输出警告,让您可以修改代码以避免错误。在更正代码后,消除所有警告,当程序运行时不会发生此​​特定错误。

您可以使用多个类型检查的模块,每个模块检查不同类型的错误。通过这种方式,你可以建立在 Java 类型系统之上,随时随地添加您想要的特定检查。

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

在很多情况下,你不必写自己的类型检查模块。第三方组织已经在做这个工作了。例如,华盛顿大学(the University of Washington)创建的 Checker Framework 。该框架包括一个 NonNull 模块,以及一个正则表达式模块和互斥锁模块。欲了解更多信息,请参见 Checker Framework

重复注解

若注解包含相同的类型,则被称为重复注解(repeating annotation),这个是 Java SE 8 之后所支持的。

比如,你正在编写的代码使用计时器服务,使您能够在特定的时间或在某个计划,类似于 UNIX cron 服务运行的方法。现在,你要设置一个计时器,在下午 11:00 运行的方法,doPeriodicCleanup,在每月和每周五的最后一天要设置定时运行,创建一个@Schedule注释,并两次将其应用到了 doPeriodicCleanup 方法。在第一次使用指定月的最后一天和第二指定星期五在下午11点,使用如下:

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

上面的示例是将注解应用在方法上。你可以在任何使用标准的注解地方使用重复注解。例如,你有一个类来处理未授权的访问异常。有一个@Alert注解的类标注为管理人员和另一个用于管理员:

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

由于兼容性的原因,重复的注释被存储在一个由 Java 编译器自动产生的容器注解(container annotation)里。为了使编译器要做到这一点,你的代码里两个声明都需要。

第一步:声明一个重复注解

重复注解用 @Repeatable 元注解标记。下面例子定义一个自定义的 @Schedule 重复注解:

  1. import java.lang.annotation.Repeatable;
  2. @Repeatable(Schedules.class)
  3. public @interface Schedule {
  4. String dayOfMonth() default "first";
  5. String dayOfWeek() default "Mon";
  6. int hour() default 12;
  7. }

@Repeatable元注解的值是由 Java 编译器生成存储重复注解的容器注解的类型。在本例中,容器注解的类型是 Schedules,所以重复注解 @Schedule 被存储在@Schedules 注解中。

应用相同注解到声明但没有首先声明它是可重复的,则在编译时会出错。

步骤2:声明容器注解类型

容器注解类型必须有数组类型的元素 value,而数组类型的组件类型必须是重复注解类型,示例如下:

  1. public @interface Schedules {
  2. Schedule[] value();
  3. }

检索注解

反射 API 有几种方法可用于检索注解。返回单个注解的方法的行为,如AnnotatedElement.getAnnotationByType(Class),如果所请求的类型的注解类型只存在一个则仅返回一个注解,则该行为是没有改变的。如果有多个请求类型的注解类型存在,则可以通过先得到他们的容器注解从而获取它们。这种方式下,传统代码继续工作。其他方法是在 Java SE 8 中,通过容器注释扫描到一次返回多个注解,如 AnnotatedElement.getAnnotations(Class)。见AnnotatedElement 类的规范,查看所有的可用方法的信息。

设计考虑

当设计一个注解类型,你必须考虑到该类型的注解的基数(cardinality)。现在可以使用一个注释零次,一次,或者,如果注解的类型被标以@Repeatable,则不止一次。另外,也可以通过使用@Target元注解来限制注解类型在哪里使用。例如,您可以创建一个只能在方法和字段使用可重复的注解类型。精心设计的注解类型是非常重要的,要确保使用注解的程序员感觉越灵活和强大越好。

示例

如何定义注解

我们自定义了一个注解 MyAnnotation,用来标识我们是什么公司:

  1. @Documented
  2. @Retention(RUNTIME)
  3. public @interface MyAnnotation {
  4. String company() default "waylau.com";
  5. }

该注解只有一个方法声明 company(),默认值是字符串“waylau.com”。

如何使用注解

下面演示下如何用这个注解。

我们在测试类 AnnotationTest 的方法上加上了我们的注解,并设了值“www.waylau.com”:

  1. class AnnotationTest {
  2. @MyAnnotation(company="https://waylau.com")
  3. public void execute(){
  4. System.out.println("do something~");
  5. }
  6. }

如果获取注解的信息

通过反射机制,我们可以获取到注解的信息:

  1. AnnotationTest test = new AnnotationTest();
  2. test.execute();
  3. // 获取 AnnotationTest 的Class实例
  4. Class<AnnotationTest> c = AnnotationTest.class;
  5. // 获取需要处理的方法Method实例
  6. Method method = c.getMethod("execute", new Class[]{});
  7. // 判断该方法是否包含 MyAnnotation 注解
  8. if(method.isAnnotationPresent(MyAnnotation.class)){
  9. // 获取该方法的 MyAnnotation 注解实例
  10. MyAnnotation myAnnotation = method.getAnnotation(MyAnnotation.class);
  11. // 执行该方法
  12. method.invoke(test, new Object[]{});
  13. // 获取 myAnnotation 的属性值
  14. String company = myAnnotation.company();
  15. System.out.println(company);
  16. }
  17. // 获取方法上的所有注解
  18. Annotation[] annotations = method.getAnnotations();
  19. for(Annotation annotation : annotations){
  20. System.out.println(annotation);
  21. }
  22. }

执行,正常情况下能看到如下打印信息: