Annotation 对程序运行没有影响,它的目的在于对编译器或分析工具说明程序的某些信息,您可以在包、类、方法、域成员等加上 Annotation。每一个 Annotation 对应于一个实际的 Annotation 类型。
1、限定 Override 父类方法 @Override
java.lang.Override
是 J2SE 5.0 中标准的 Annotation 类型之一,它对编译器说明某个方法必须是重写父类中的方法。编译器得知这项信息后,在编译程序时如果发现被 @Override 标示的方法并非重写父类中的方法,就会报告错误。
举个例子来说,如果在定义新类时想要重写 Object 类的 toString() 方法,可能会写成这样:
1 | public class User { |
在编写 toString() 方法时,因为输入错误或其他的疏忽,将之写成 ToString() 了,编译这个类时并不会出现任何的错误,编译器不会知道你是想重写 toString() 方法,只会以为是定义了一个新的 ToString() 方法。
可以使用 java.lang.Override 这个 Annotation 类型,在方法上加上一个 @Override 的 Annotation,这可以告诉编译器现在定义的这个方法,必须是重写父类中的同名方法。
1 | public class User { |
在编译程序时,编译器看到 @Override 这个 Annotation,了解到必须检查被标示的方法是不是重写了父类的 ToString() 方法,但父类中并没有 ToString() 这个方法,所以编译器会报告错误:
1 | Method does not override method from its superclass |
重新修改一下例子中的 ToString() 为 toString(),编译时就不会有问题了。
1 | public class User { |
java.lang.Override 是一个 Marker Annotation,简单地说就是用于标示的 Annotation,Annotation 名称本身即表示了要给工具程序的信息。例如 Override 这个名称告知编译器,被 @Override 标示的方法必须是重写父类中的同名方法。
Annotation 类型与 Annotation 实际上是有区分的,Annotation 是 Annotation 类型的实例。例如 @Override 是个 Annotation,它是 java.lang.Override 类型的一个实例,一个文件中可以有很多个 @Override,但它们都是属于 java.lang.Override 类型。
2、标示方法为 Deprecated @Deprecated
java.lang.Deprecated
是 J2SE 5.0 中标准的 Annotation 类型之一,它对编译器说明某个方法已经不建议使用。如果有开发人员试图使用或重写被 @Deprecated 标示的方法,编译器必须提出警告信息。
举个例子来说,你可能定义了一个 Utils 类,并在其中定义 getRandom() 方法,而在这个类被实际使用一段时间之后,不建议开发人员使用 getRandom() 方法了,并想将这个方法标示为 deprecated。这时可以使用 @Deprecated 在 getRandom() 方法加上标示。
1 | package com.sunzn.utils; |
如果有人试图在继承这个类后重写 getRandom() 方法,或是在程序中调用 getRandom() 方法,则编译时会有警告出现,如下所示。
1 | package com.sunzn.utils; |
编译 Tools 时,就会出现一下的警告:
1 | Note:Tools.java uses or override a deprecated API. |
java.lang.Deprecated 也是一个 Marker Annotation,简单地说就是用于标示。Annotation 名称本身即包括了要给工具程序的信息,例如 Deprecated 这个名称在告知编译器,被 @Deprecated 标示的方法是一个不建议被使用的方法,如果开发人员不小心使用了被 @Deprecated 标示的方法,编译器要提出警告提醒开发人员。
3、抑制编译器警告 @SuppressWarnings
java.lang.SuppressWarnings
是 J2SE 5.0 中标准的 Annotation 类型之一,它对编译器说明某个方法中若有警告信息,则加以抑制,不用在编译完成后出现警告。
下面说明 @SuppressWarnings 的功能,新建一个 SomeClass 类。
1 | package com.sunzn.utils; |
由于在 J2SE 5.0 中加入了集合对象的泛化功能,并建议明确指定集合对象中将内嵌的对象的类型,但在 SomeClass 类中使用 Map 时并没有指定内嵌对象的类型,所以在编译时会出现以下的信息:
1 | Note:SomeClass.java uses unchecked or unsafe operations. |
如果让编译器忽略这些细节,则可以使用 @SuppressWarnings 这个 Annotation。
1 | package com.sunzn.utils; |
这样,编译器将忽略 unchecked 的警告,你也可以指定忽略多个警告:
1 |
理想上是可以抑制警告的,不过在 Sun JDK 5.0 上并没有实现这个 Annotation 的功能,所以 @SuppressWarnings 实际上并没有作用。如果真要关掉警告,那么在编译时加上 -nowarn 是另一个方法,只不过这样会关掉所有的警告信息。
@SuppressWarnings 是所谓的 Single-Value Annotation,因为这样的 Annotation 只有一个成员,成为 value 成员,可在使用 Annotation 时作额外的信息指定。
4、自定义 Annotation 类型
可以自定义 Annotation 类型,并使用这些自定义的 Annotation 类型在程序代码中使用 Annotation,这些 Annotation 将提供信息给程序代码分析工具。
首先来看看如何定义 Marker Annotation,也就是 Annotation 名称本身即提供信息。对于程序分析工具来说,主要是检查是否有 Marker Annotation 的出现,并做出对应的动作。要定义一个 Annotation 所需的动作,就类似于定义一个接口,只不过使用的是 @interface。下面的例子定义了一个 Debug Annotation 类型。
1 | package com.sunzn.annotation; |
由于是一个 Marker Annotation,所以没有任何成员在 Annotation 定义中。编译完成后,就可以在程序代码中使用这个 Annotation。例如:
1 | package com.sunzn.annotation; |
稍后可以看到如何在 Java 程序中取得 Annotation 信息(因为要使用 Java 程序取得信息,所以还要设定 meta-annotation,稍后会谈到)。接着来看看如何定义一个 Single-Value Annotation,它只有一个 value 成员。例如:
1 | package com.sunzn.annotation; |
实际上定义了 value() 方法,编译器在编译时会自动产生一个 value 的域成员,接着在使用 UnitTest Annotation 时要指定值。例如:
1 | package com.sunzn.annotation; |
@UnitTest(“SUBTRACTION”) 实际上是 @UnitTest(value = “SUBTRACTION”) 的简单写法,value 也可以是数组值。例如定义一个 FunctionTest 的 Annotation 类型。
1 | package com.sunzn.annotation; |
在使用 FunctionTest Annotation 时,可以写成 @FunctionTest({“method1”, “method2”}) 这样的简便形式,或是 @FunctionTest(value = {“method1”, “method2”}) 这样的详细形式,也可以对 value 成员设定默认值,使用 default 关键词即可。
1 | package com.sunzn.annotation; |
这样如果使用 @UnitTest 时没有指定 value 值,则 value 默认就是 noMethod。
也可以为 Annotation 定义额外的成员,以提供额外的信息给分析工具。下面的例子使用枚举类型、String 与 boolean 类型来定义 Annotation 的成员。
1 | package com.sunzn.annotation; |
下面看看如何使用 @Process Annotation 类型。
1 | package com.sunzn.annotation; |
当使用 @interface 自行定义 Annotation 类型时,实际上是自动继承了 java.lang.annotation.Annotation 接口,并由编译器自动完成其他产生的细节,并且在定义 Annotation 类型时,不能继承其他的 Annotation 类型或接口。
定义 Annotation 类型时也可以使用包机制来管理类。由于范例所设定的包都是 com.sunzn.annotation,所以可以直接使用 Annotation 类型名称而不指定包名,但如果是在别的包下使用这些自定义的 Annotation,记得使用 import 告诉编译器类型的包位置。例如:
1 | package com.sunzn.test; |
或是使用完整的 Annotation 名称,例如:
1 | package com.sunzn.test; |
5、meta-annotation
所谓 meta-annotation 就是 Annotation 类型的数据,也就是 Annotation 类型的 Annotation。在定义 Annotation 类型时,为 Annotation 类型加上 Annotation 并不奇怪,这可以为处理 Annotation 类型的分析工具提供更多的信息。
5.1、告知编译器如何处理 annotation @Retention
java.lang.annotation.Retention
类型可以在你定义 Annotation 类型时,指示编译器该如何对待自定义的 Annotation 类型,编译器默认会将 Annotation 信息留在 .class
文件中,但不被虚拟机读取,而仅用于编译器或工具运行时提供信息。
在使用 Retention 类型时,需要提供 java.lang.annotation.RetentionPolicy
的枚举类型。RetentionPolicy 的定义如下所示:
1 | package java.lang.annotation; |
RetentionPolicy 为 SOURCE 的例子是 @SuppressWarnings,这个信息的作用仅在编译时期告知编译器来抑制警告,所以不必将这个信息存储在 .class 文件中。
RetentionPolicy 为 RUNTIME 的时机,可以像是你使用 Java 设计一个程序代码分析工具,你必须让 VM 能读出 Annotation 信息,以便在分析程序时使用,搭配反射机制,就可以达到这个目的。
J2SE 5.0 新增了 java.lang.reflect.AnnotatedElement 接口,其中定义有4个方法:
- public Annotation getAnnotation(Class annotationType);
- public Annotation[] getAnnotations();
- public Annotation[] getDeclaredAnnotations();
- public boolean isAnnotationPresent(Class annotationType)
Class、Constructor、Field、Method、Package 等类,都实现了 AnnotatedElement 接口,所以可以从这些类的实例上,分别取得标示其上的 Annotation 与相关信息。由于是在执行时期读取 Annotation 信息,所以定义 Annotation 时必须设定 RetentionPolicy 为 RUNTIME,也就是可以在 VM 中读取 Annotation 信息。
举个例子来说,假如设计了如下的 Annotation。
1 | package com.sunzn.annotation; |
由于 RetentionPolicy 为 RUNTIME,编译器在处理 SomeAnnotation 时,会将 Annotation 及给定的相关信息编译至 .class 文件中,并设定为 VM 可以读出 Annotation 信息。接着可以如下面的范例来使用 SomeAnnotation。
1 | package com.sunzn.annotation; |
现在假设要设计一个源代码分析工具来分析所设计的类,一些分析时所需的信息已经使用 Annotation 标示于类中了,可以在执行时读取这些 Annotation 的相关信息。下面提供一个简单的范例。
1 | package com.sunzn.annotation; |
若 Annotation 标示于方法上,就要取得方法的 Method 代表实例。同样地,如果 Annotation 标示于类或包上,就要分别取得类的 Class 代表实例或是包的 Package 代表实例。之后可以使用实例上的 getAnnotations() 等相关方法,以测试是否可取得 Annotation 或进行其他操作。AnalysisApp 的执行结果如下:
1 | 找到 |
5.2、限定 annotation 使用对象 @Target
在定义 Annotation 类型时,使用 java.lang.annotation.Target 可以定义其适用的时机。在定义时要指定 java.lang.annotation.ElementType 的枚举值之一:
1 | package java.lang.annotation; |
举个例子来说,假设定义 Annotation 类型时,要限定它只能适用于构造函数和方法成员,则可以如下例所示的方法来定义。
1 | package com.sunzn.annotation; |
如果尝试将 MethodAnnotation 标示于类之上,例如:
1 | package com.sunzn.annotation; |
则在编译时会发生以下的错误:
1 | SomeClass.java:1: annotation type not applicable to this kind of declaration |
5.3、要求为 API 文件的一部分 @Documented
在制作 Java Doc 文件时,并不会默认将 Annotation 的数据加入到文件中。例如设计了以下的 OneAnnotaion 类型:
1 | package com.sunzn.annotation; |
然后将之用在以下的程序中:
1 | package com.sunzn.annotation; |
可以试着使用 javadoc 程序来生成 Java Doc 文件,会发现文件中并不会有 Annotation 的相关信息,如下图所示。
Annotation 用于标示程序代码以便分析工具使用相关信息,有时 Annotation 包括了重要的信息,你也许会想要在使用者制作 Java Doc 文件的同时,也一并将 Annotation 的信息加入至 API 文件中。所以在定义 Annotation 类型时,可以使用 java.lang.annotation.Documented。下面是一个简单的范例。
1 | package com.sunzn.annotation; |
使用 java.lang.annotation.Documented 为定义的 Annotation 类型加上 Annotation 时,必须同时使用 Retention 来指定编译器将信息加入 .class 文件,并可以由 VM 读取,也就是要设定 RetentionPolicy 为 RUNTIME。接着可以使用这个 Annotation,并产生 Java Doc 文件,这次可以看到文件中包括了 @TwoAnnotation 的信息,如图所示。
5.4、子类是否继承父类的 annotation @Inherited
在定义 Annotation 类型并使用于程序代码上后,默认父类中的 Annotation 并不会被继承至子类中。可以在定义 Annotation 类型时加上 java.lang.annotation.Inherited 类型的 Annotation,这让你定义的 Annotation 类型在被继承后仍可以保留至子类中。
1 | package com.sunzn.annotation; |
可以在下面这个程序中使用 @ThreeAnnotation:
1 | package com.sunzn.annotation; |
如果有一个类继承了 SomeClass 类,则 @ThreeAnnotation 也会被继承下来。