Annotations
Annotation
是一种元数据,它为程序提供不属于自身的信息,对被注解代码的操作不起直接作用。
注解有很多用处,包括:
- 提供给编译器的信息 —— 编译器可以根据注解检测错误或抑制警告
- 编译期和部署期处理 —— 工具可以根据注解生成代码、XML 等文件
- 运行时处理 —— 有些注解可以在运行时被检测到
下文介绍注解可以被用到什么地方,如何使用,JavaSE 提供了哪些预定义注解,怎样在可插拔类型系统中使用类型注解实现强大的类型检查,怎样实现重复注解。
Annotation Basics
The Format of an Annotation
下面是最简单的注解形式:
@Entity
@
符号告诉编译器,紧跟着的是注解。下面这个注解的名称是 Override
:
@Override
void mySuperMethod(){}
注解可以包含 elements
,它们可以是 named
或 unnamed
,具有 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.lang
或 java.lang.annotation
中定义的一种,上面的 Override
和 SuppressWarnings
就是预定义注解。你还可以定义自己的注解类型,就如上面的 Author
和 Ebook
。
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 语言规范中列举了两类:deprecated
和 unchecked
。当与引入 泛型 之前的遗留代码交互时,unchecked
警告就会产生。使用下面的语法抑制多种类型警告:
@SuppressWarnings({"unchecked", "deprecation"})
@SafeVarargs 用于方法或构造器时,断言该方法不会对自身的 varargs
产生潜在的不安全操作。此时,与 varargs 使用
相关的 unchecked warnings
将被抑制。
@FunctionalInterface 注解在 Java SE 8
引入,表明要将类型声明为 Java 语言规范定义的函数接口。
Annotation That Apply to Other Annotations
应用于其他注解的注解称为 meta-annotations
。java.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 表明该注解类型可以从父类继承(默认情况下不可以)。当用户查询一个类的注解类型而该类没有注解时,该类的父类将被查找。这种注解仅用于类型声明。
@Repeatable 在 Java SE 8
被引入,被它标记的注解,在相同声明或类型引用处可以使用多次。更多信息见 Repeating Annotations。
Type Annotations and Pluggable Type Systems
Java SE 8 发布前,注解仅能被用到声明上。随着 Java SE 8 的发布,注解也可以用到任何 类型引用
上了。这意味着你可以在任何使用类型的地方使用注解。几个类型使用的例子是 类实例创建表达式
、casts
、implements
子句,和 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: 为增强请求定义一个注解,元素是 id
,synopsis
,engineer
和 date
。为 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。