注解
1. 概念
Annotation
即注解(也被翻译为注释),注解提供了一种为程序元素设置元数据的方法,从某些方面来看,注解就像修饰符一样,可用于修饰包、类、构造器、方法、成员变量、参数、局部变量的声明,这些信息被存储在注解的"name=value"对中。
注解是一个接口,程序可以通过反射来获取指定程序元素的java.lang.annotation.Annotation
对象,然后通过java.lang.annotation.Annotation
对象来取得注解里的元数据。
注解被用来为程序元素(类、方法、成员变量等)设置元数据,值得指出的是,注解不影响程序代码的执行,无论增加/删除注解,代码都始终如一地执行。如果希望让程序中的注解在运行时起一定的作用,只有通过某种配套的工具对注解中的信息进行访问和处理,访问和处理注解的工具统称APT(Annotation Processing Tool)。
2. 注解定义
每个注解都必须通过一个注解接口进行定义,这些接口中的方法与注解中的参数相对应。定义一个注解的语法与定义接口非常像:
// 空注解
修饰符 @interface Test {
}
// 注解还可以带参数:参数在注解定义中以“无形参的方法”这样的形式来声明
public @interface MyTag {
// 定义带4个参数的注解
// 注解中的参数以方法的形式来定义
String name();
int age();
// 定义参数时还可以为它们指定初始值(默认值),使用`default`关键字
String address() default "河南省";
boolean married() default false;
}
所有的注解会自动继承
java.lang.annotation.Annotation
(这个接口是一个常规接口,并非一个注解接口),同时不能继承别的类或是接口。参数成员都是
public
的。参数成员只能用如下类型:
- 基本数据类型
char, boolean, byte, short, int, long, float, double
; String, Enum, Class, Annotation
数据类型;- 以及上述类型的数组。
- 基本数据类型
要获取类方法和字段的注解信息,必须通过Java的反射技术来获取
Annotation
对象,除此之外没有别的获取注解对象的方法。
定义注解的参数时还可以为其指定初始值(默认值),指定参数的初始值可使用default
关键字。
@interface MyTag {
String name() default "chuan";
int age() default 32;
}
class Test {
// 参数有默认值,所以可以不为它的参数指定值
@MyTag
public void info() {
// ...
}
// ...
}
(Java规定?)一个注解参数永远不能被设置为null
,甚至不允许其默认值为null
。这样在实际应用中会相当不方便,你必须使用其他的默认值,如""
或Void.class
。
3. 使用注解
使用注解时要在其前面增加@
符号,并把该注解当成一个修饰符使用,用于修饰它支持的程序元素。
使用注解时的语法非常类似于public
, final
这样的修饰符,通常可用于修饰程序中的类、方法、变量、接口等定义。通常会把注解放在所有修饰符之前,而且由于使用注解时可能还需要为参数指定值,因而注解的长度可能较长,所以通常把注解另放一行,如下:
@Test
public class MyClass {
// ...
}
如果在注解里定义了参数,可通过如下方式进行传参:
@interface MyTag {
// 定义带两个参数的注解
String name();
int age();
}
class Test {
@MyTag(name = "xx", age = 6)
public void info() {
// ...
}
}
因为注解是由编译器计算而来的,因此所有参数值必须是编译器常量。
如果参数值是一个数组,那么要将它的值用括号{}
括起来,即:@BugReport(..., reportedBy={"Harry", "Carl"})
,如果该参数具有单值,那么可以忽略这些括号:@BugReport(..., reportedBy="Joe") // OK, same as {"Joe"}
3.1 重复修饰
如果注解的作者将其声明为可重复的,那么你就可以多次重复使用同一个注解:
@BugReport(showStopper=true, reportedBy="Joe")
@BugReport(reportedBy={"Harry", "Carl"})
public void checkRandomInsertions(){}
3.2 注解的快捷方式
如果一个参数具有特殊的名字value
,并且没有指定其他参数,那么就可以忽略掉这个参数名以及等号:
public @interface Tester {
String name() default "";
int value() default 18;
}
@Tester(27)
public class ClassA {
}
3.3 注解类型用法
声明注解提供了正在被声明的项的相关信息。例如,在下面的声明中就断言了userId
参数不为空:
public User getUser(@NonNull String userId)
@NonNull
注解是Checker Framework的一部分。通过使用这个框架,可以在程序中包含断言,例如某个参数不为空,或者某个String
包含一个正则表达式。然后,静态分析工具将检查在给定的源代码段中这些断言是否有效。
假设我们有一个类型为List<String>
的参数,并且想要表示其中所有的字符串都不为null
。这就是类型用法注解大显身手之处,可以将该注解放置到类型参数之前:List<@NonNull String>
。
类型用法注解可以出现在下面的位置:
- 与泛化类型参数一起使用:
List<@NonNull String>, Comparator.<@NonNull String> reverseOrder()
- 数组中的任何位置:
@NonNull String[][] words
:words[i][j]
不为null
String @NonNull [][]words
:words
不为null
String[] @NonNull [] words
:words[i]
不为null
- 与超类和实现接口一起使用:
class Warning extends @Localized Message
- 与构造器调用一起使用:
new @Localized String(...)
- 与强制转型和
instanceof
检查一起使用:(@Localized String) text, if (text instanceof @Localized String)
(这些注解只供外部工具使用,它们对强制类型转换和instanceof
检查不会产生任何影响) - 与异常规约一起使用:
public String read() throws @Localized IOException
- 与通配符和类型边界一起使用:
List<@Locaied ? extends Message>, List<? extends @Localized Message>
- 与方法和构造器引用一起使用:
@Localized Message::getText
也有多种类型位置是不能被注解的:
@NonNull String.class
import java.lang.@NonNull String;
3.4 注解this
假设有一个注解@ReadOnly
能够指示某一个方法形参是不可被修改的:
public class Point {
public boolean equals(@ReadOnly Object other) {
// ...
}
}
那么对于如下的调用p.equals(q)
,显然可以推理出q
没有被修改过。
但是,如果希望p
也不被修改呢?尽管在该equals
方法中,this
变量是绑定到p
的,但是this从来都没有被声明过,因此无法用注解@ReadOnly
来修饰它。此时,其实Java有一种很少使用的语法变体来声明它,可如下来注解this
:
public class Point {
public boolean equals(@ReadOnly Point this, @ReadOnly Object other) {
// ...
}
}
此时的第一个参数被称为接收器参数,它必须被命名为this
,同时它的类型就是要构建的类。
需要额外一提的是,你只能为方法而不能为构造器提供接收器参数。因为从概念上讲,构造器中的this
引用在构造器没有执行完之前还不是给定类型的对象。所以,放置在构造器上的注解描述的是被构建的对象的属性。
4. 标准注解
Java SE在java.lang
, java.lang.annotation
和javax.annotation
包中定义了大量的注解接口。
4.1 用于编译的注解
6个基本的注解如下,除了@Generated
位于javax.annotation
包,其它均定义在java.lang
包下:
注解 | 修饰的程序单元 | 作用 |
---|---|---|
@Deprecated | 全部 | 表示某个程序元素(类、方法等)已过时,编译器会发出警告 |
@SuppressWarnings | 除了包和注解之外的所有情况 | 取消显示指定的编译器警告 |
@SafeVarargs | 方法、构造器 | 断言varargs参数可安全使用,抑制堆污染警告 |
@Override | 方法 | 强制一个子类的方法必须是覆盖父类方法的方法 |
@FunctionalInterface | 接口 | 指定某个接口必须是函数式接口 |
@Generated | 全部 | 供代码生成工具来使用,任何生成的源代码都可以被注解,从而与程序员提供的代码区分开 |
4.2 用于管理资源的注解
注解 | 修饰的程序单元 | 作用 |
---|---|---|
@PostConstruct, @PreDestroy | 方法 | 被标记的方法应该在构造之后/移除之前立即被调用 |
@Resource | 类、接口、方法、域 | 在类/接口上:标记为在其他地方要用到的资源 在方法/域上:为“注入”而标记 |
@Resources | 类、接口 | 一个资源数组 |
4.3 元注解
JDK在java.lang.annotation
包下提供了6个,其中5个用于修饰其他的注解定义。
注解 | 作用 |
---|---|
@Target | 用于指定被修饰的注解能用于修饰哪些程序单元(ElementType )。一个没有被此注解限制的注解可以应用于任何项上。 |
@Retention | 用于指定被修饰的注解可以保留多长时间。当未使用该注解修饰时,默认值相当于是RetentionPolicy.CLASS |
@Documented | 修饰注解类表明其将被javadoc工具提取成文档 |
@Inherited | 修饰作用于类的注解,指定被修饰的注解具有继承性:即该注解修饰的类的子类也将同样被该注解修饰 |
@Repeatable | 指明被修饰的这个注解可以在同一个元素上应用多次 |
@Native | 修饰成员变量,表示这个变量可以被本地代码引用,常常被代码生成工具使用(此注解不常用,了解即可) |
将一个注解应用到它自身上是合法的。例如,@Documented
注解被自身为注解为@Documented
。因此,针对注解的Javadoc文件可以表明它们是否可被归档。
ElementType
关于程序单元的枚举类型ElementType
:
TYPE
:类、接口、注解、枚举FIELD
:字段(含枚举值)METHOD
:方法PARAMETER
:形参(Formal parameter declaration)CONSTRUCTOR
:构造方法LOCAL_VARIABLE
:局部变量ANNOTATION_TYPE
:注解类型PACKAGE
:包包是在文件
package-info.java
中注解的,该文件只包含以注解先导的包语句:/** Package-level Javadoc */ @GPL(version="3") package com.horstmann.corejava; import org.gnu.GPL;
TYPE_PARAMETER
:类型参数TYPE_USE
:类型用法
RetentionPolicy
用于@Retention
注解的保留策略:
保留规则 | 描述 |
---|---|
SOURCE | 不包括在类文件中的注解 |
CLASS | 包括在类文件中的注解,但是虚拟机不需要将它们载入 |
RUNTIME | 包括在类文件中的注解,并由虚拟机载入。通过反射API可获得它们 |
@Inherited 详解
如果一个类具有继承注解(即被@Inherited
修饰的注解),那么它的所有子类都自动具有相同的注解。
这使得创建一个与
Serializable
这样的标记接口具有相同运行方式的注解变得很容易。实际上,@Serializable
注解应该比没有任何方法的Serializable
标记接口更适用。一个类之所有可以被序列化,是因为存在着对它的成员域进行读写的运行期支持,而不是因为任何面向对象的设计原则。注解比接口继承更擅长描述这一事实(当然,可序列化接口是在JDK1.1中产生的,远比注解出现得早)。假设定义了一个继承注解
@Persistent
来指定一个类的对象可以存储到数据库中,那么该持久类的子类就会自动被注解为是持久性的。@Inherited @interface Persistent { } @Persistent class Employee { } class Manager extends Employee { // also @Persistent }
@Repeatable 详解
package java.lang.annotation;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Repeatable {
/**
* Indicates the <em>containing annotation type</em> for the
* repeatable annotation type.
* @return the containing annotation type
*/
Class<? extends Annotation> value();
}
对于Java SE 8来说,将同种类型的注解多次应用于某一项是合法的。为了向后兼容,可重复注解的实现者需要提供一个容器注解,它可以将这些重复注解存储到一个数组中:
@Repeatable(TestCases.class)
@interface TestCase {
String params();
String expected();
}
@interface TestCases {
TestCase[] value();
}
无论何时,只要用户提供了两个或更多个@TestCase
注解,那么它们就会自动地被包装到一个@TestCases
注解中。
在处理可重复注解时必须非常仔细。如果调用getAnnotation
来查找某个可重复注解,而该注解又确实重复了,那么就会得到null
,这是因为重复注解被包装到了容器注解中。在这种情况下,应该调用getAnnotationByType
,这个调用会被“遍历”容器,并给出一个重复注解的数组。如果只有一条注解,那么该数组的长度为1。通过使用这个方法,就不用操心如何处理容器注解了。
5. 提取注解信息
Java使用java.lang.annotation.Annotation
接口来代表注解,该接口是所有注解的父接口。
public interface Annotation {
boolean equals(Object obj);
int hashCode();
String toString();
Class<? extends Annotation> annotationType();
}
在java.lang.reflect
包下主要包含一些实现反射功能的工具类,其中含有的AnnotatedElement
接口代表程序中可以接受注解的程序元素,其主要有如下几个实现类:
类 | 含义 |
---|---|
Class | 类定义 |
Constructor | 构造器定义 |
Field | 类的成员变量定义 |
Method | 类的方法定义 |
Package | 类的包定义 |