注解(Annotation)是对程序的一种描述和说明,可以理解为是程序的元数据(metadata),它对被注解的代码没有直接的影响,但是我们可以通过反射机制获取注解,然后让处理这些注解称为程序的一部分。本文介绍注解的基本内容。
1. 从一个例子开始:
我们在重写(Override)一个方法时,经常会这样写:
<span style="background-color: rgb(255, 255, 51);">@Override</span>
public void close() throws Exception {
// TODO Auto-generated method stub
}
上面黄色背景的@Override就是一种注解,它对close()这样方法作出说明:这个方法是重写超类或者接口的方法。即注解是用来说明程序本身的数据。把注解(Annotation)和注释(comment)区分开来很重要,注释一般是写给开发人员(使用你的代码的人)看的,帮助别人理解你的代码,对程序毫无影响,编译器对注释一无所知,编译的时候完全忽略注释。但是注解可以被编译器甚至程序所使用的。其实在@Override背后的,是一个注解类型(annotation type),就像类是一种类型一样。这个注解类型再java.lang.Override.java中定义,查看源码如下:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
这就是Override这个注解类型的定义,注意注解是一种类型(type),与类、接口、枚举是一个概念层次的。实际上你在Eclipse使用右键“新建”菜单的时候,你会看到下面的界面:
我们看到了Annotation、Class、Enum、Interface。我想表达的意思就是注解它也是一种类型,后面我们会看到可以把它看做是一种特殊的接口,就像Enum看作是一种特殊的类一样。
2. 注解的用途
定义注解类型或者直接使用预定义的注解类型有什么用途,其中包括:
2.1 information for compiler
编译器可以根据注解来检测代码错误或者给出警告信息。以Override注解为例,如果你想重写父类方法或者实现接口方法,在开始定义方法之前先使用@Override注解,那么如果你定义方法时给出的方法名或者参数类型与父类(接口)中定义的不一样,则编译器无法通过。比如,
/**
* @author Brandon B. Lin
*
*/
public class MyThread extends Thread {
@Override
public void run(int a){
}
@Override
public void rnn(){
}
}
我们继承了Thread类,并且在重写方法之前使用注解@Override,但是在定义方法的时候,由于拼写错误或者其他原因导致方法签名与父类不匹配,那么会出现编译错误。如果你没有使用注解,那么编译器理解为你在子类重新添加了一个方法,于是编译正常通过,即使方法名只相差一个字符。
2.2 compile-time and deploy-time processing
开发和部署工具可以根据注解自动生成代码、XML文件等其他文件。
2.3 runtime processing
如果注解在运行时依然存在,那么可以通过反射机制获取注解内容,并对注解进一步处理,从而“融入”到程序中。
3. 定义和使用注解
3.1 定义注解
跟定义一个类一样,定义一个注解也需要遵循Java语言规范的格式。为了定义一个注解,使用@interface代替类定义中的class关键字。例如,为了描述一个类的元数据,我们定义一个简单的注解:
/**
* @author Brandon B. Lin
*
*/
public @interface ClassAnnotation {
String version();
String lastModified() default "N/A";
String reviewer();
}
访问控制符public,然后关键字@interface,注解名称,{ }.在注解里面,定义了注解的成员(或者说是属性),虽然它看起来更像是方法的声明,注意没有方法体,方法体有Java自动实现。注意到属性lastModied后面有个default “N/A”,顾名思义,就是为属性提供一个默认值。定义好之后,我们就可以使用了:
/**
* @author Brandon B. Lin
*
*/
@ClassAnnotation(version = "1.0", lastModified = "2014/5/1", reviewer = "John, Gil, Brandon")
public class UseAnnotation {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
}
}
在定义类之前,使用注解,然后将注解属性的值放在()中,用逗号隔开。注意使用注解时,如果不在同一个包,也必须使用import语句导入,并且能否导入收到注解访问控制符的限制,限制的规则同类访问控制符是一样的。
3.2 几点说明
1)注解不能使用extends关键字继承,所有的注解都自动拓展java.lang.annotation.Annotation接口,Annotation是所有注解的超接口。注解继承接口,好像有点难以理解,但是我们前面说到,可以把注解看做是一种特殊的接口。需要注意的是,如果一个接口手动地继承了Annotation接口,它并没有定义注解类型。Annotation接口本身也不是注解类型,不能使用。Annotation接口声明了几个方法:annotationType,hashCode,equals,toString。简单介绍一些这几个方法:
Class<<span style="background-color: rgb(255, 255, 0);">? extends Annotation</span>> annotationType()
该方法返回表示该注解的Class对象,java.lang.Class这个类时用来抽象java当中的类型的,包括类、接口、枚举和注解。用来抽象注解的时候,表示的是注解的类型。注意Class本身就是一个类,假如我们定义了三个接口Annotation1、Annotation2、Annotation3,那么这三个注解类型的对象调用annotationType方法的时候,分别返回Class类的三个对象,我们几位obj1、obj2、obj3。因此关系就很明了了,Class类用来抽象注解类型,而可能存在很多个注解类型,预定义的,自己定义的,每一个注解类型就用一个Class对象来描述,比如obj1、obj2、obj3分别描述注解Annotation1,Annotation2/Annotation3. 再看看返回类型,这里涉及到泛型了,尖口号的内容是Class类的类型参数(type parameter),并且使用通配符?和extends对类型参数进行限制,即返回的Class类型的类型参数必须是Annotation或者其子类型,而所有注解都自动拓展Annotation接口,所以都是符合要求的,比如上面的ClassAnnotation对象调用annotationType返回的对象就属于Class<ClassAnnotation>类型。
每个类型对应一个Class对象,同一个类型的所有对象返回的Class对象都相同,比如:
public static void main(String[] args) {
Throwable t1 = new Throwable();
Throwable t2 = new Throwable();
System.out.println(t1.getClass() == t2.getClass());
System.out.println(t1.getClass() == Throwable.class);
}
最终输出都是true。Throwable类型的对象t1和t2获得的Class<Throwable>对象都是同一个对象,所以也可以直接通过Throwable.class获得。注意.class不是方法。
String toString()
返回表示该注解对象的字符串,返回的格式取决于具体的实现,但是一般格式为:
@com.acme.util.Name(first=Alfred, middle=E., last=Neuman)
hashCode和equals方法同其他类型一样。API文档中说toString、hashCode、equals方法是重写了类Object中对应的方法,也就说,java中的类型,无论是类、接口还是枚举、注解都最终扩展自Object类,其方法在各种类型中均可使用。
2)默认值
在定义注解的成员是,如果使用default为成员(element)提供默认值,那么在使用注解的时候,可以不为这个成员提供对应的值(当然也可以提供)。默认成员的一般形式为:
type member( ) default value;
value的类型必须与type兼容。例如,对于注解:
public @interface MyAnno {
String str() default "test";
int val() default 900;
}
在使用这个注解的时候,可以有以下4中方式:
@MyAnno()
@MyAnno(str = "else")
@MyAnno( val = 100)
@MyAnno(str = "else", val = 100)
在使用注解时,对于成员我们一般按照注解定义中的顺序给出值,但是也可以不按这个顺序来。
4 几种特殊的注解
注解中,有些注解跟常规注解不太一样,下面介绍几种特殊的注解类型
4.1 单成员注解
如果一个注解类型中的成员只有一个,称为单成员注解。在这种情况下,我们可以按照常规的注解方式定义成员、指定默认值以外,使用注解的时候可以不指定成员的名称,前提是把唯一的成员名称设为value。
public @interface SingleAnno {
String value();
}
使用这个单成员的时候可以这么来:
@SingleAnno("TT")
可以不指定成员名称,可以的意思就是你要指定也没有问题。如果成员不止一个,但是其他成员都有默认值,也可以不指定value这个成员的名称,直接使用上面的形式。例如成员为 String value(); int a() default 0; 一样可以使用@SingleAnno(“TT”)这样的形式。
总结起来就是,如果只有一个成员并且其名称为value,则使用注解时可以不指定成员名称而直接给出值。如果除了value还有其他成员,在其他成员都有默认值的情况下,也可以不指定value的名称。
4.2 注解的注解(元注解meta-annotation)
顾名思义就是用来注解注解的注解,哈哈。比如下面这个注解中:
<span style="background-color: rgb(255, 255, 0);">@Retention</span>(RetentionPolicy.SOURCE)
public @interface MyAnno {
String str() default "test";
int val() default 900;
}
我们定义了一个注解MyAnno,同时使用另一个注解Retention对MyAnno进行注解,Retention就称为注解的注解。另外,我们可以指定某个注解可以被用到什么地方,比如我们可以指定MyAnno只能用于注解方法,这是通过注解Target指定的。Target是一个内置的注解类型,部分代码如下:
public @interface Target {
/**
* Returns an array of the kinds of elements an annotation type
* can be applied to.
* @return an array of the kinds of elements an annotation type
* can be applied to
*/
ElementType[] value();
}
它是个单成员注解,也是注解的注解,其成员value指定被注解的注解可以在哪些地方使用。value是一个数组,每个数组元素的取值为枚举类型java.lang.annotation.ElementType的枚举常量。比如,对于Target注解,它使用自身指定可以使用它的程序元素:
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target { ...
可以看到,Target这个注解只能用于注解注解
(本身或者其他),其他地方都不能使用。再比如Override注解只能用于注解方法,所以在Override的定义像这样子:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
4.3 标记注解
名称也很直接,用于标记。标记(maker)注解没有成员,比如我们经常看到的一个标注注解是Deprecated,它用于指示被注解的内容已经过时,被新的形势取代,看一下部分源码:
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}
可以看到成员是空的。(PS:如果你发现value后面的值很奇怪,看一下源码会发现是因为使用了静态导入),使用了Deprecated标记注解就说明该元素(比如说方法)已经过时了,相反的,没有使用该标记就是没有过时。因此,使用标记注解的时候,无需给出成员的值,()都可以省略。标注注解用于二元情况,即非黑即白,要么过时要么不过时。比如另一个标记注解Documented,这个注解也是注解的注解,在某个注解中使用了Documented注解,就表明这个注解将被文档化,反之不使用则表示不会被文档化。
5. 在运行时使用反射获取注解
目前为止,注解除了上面所说的可以为编译器检测错误提供信息以外,似乎没有什么用处。虽然设计注解的主要目的是用于其它开发工具和部署工具,但是我们可以在运行时通过反射机制获得注解。有关反射的API在java.lang.reflect中定义。
使用反射的第一步是获取一个Class对象,关于Class类已经在3.2中介绍,可以使用object.getClass( )或者TypeName.class获取对应的Class对象。获取Class对象之后获取方法,获取方法之后获取注解。在给出实例之前,必须先介绍一下注解的保留策略。
5.1 注解保留策略
我们知道注释在编译的时候会被编译器丢掉,而对于注解来说,可以在编译的时候丢掉,也可以让编译器将它保留到字节码文件中。这是通过注解的保留策略(retention policy)来设置的。在定义一个注解的时候,我们可以指定其保留策略。例如:
<span style="background-color: rgb(255, 255, 0);">@Retention(RetentionPolicy.SOURCE)</span>
public @interface MyAnno {
String str() default "test";
int val() default 900;
}
Retention在java.lang.reflect包中定义,只能用于注解自身或者其他注解,它用来指定某一注解的生存时间,部分源码如下:
public @interface Retention {
/**
* Returns the retention policy.
* @return the retention policy
*/
RetentionPolicy value();
}
这是一个单成员注解,并且使用了value名称,所以使用的时候可以不指出成员名称。唯一成员的类型是枚举类型RetentionPolicy。这个枚举类型中定义了三个常量SOURCE、CLASS、RUNTIME,含义如下:
SOURCE:如果Retention成员取值为SOURCE,表明该注解只存在源代码当中,在编译的时候会被编译器丢掉。
CLASS:表示注解会被存到字节码文件.class中,但是虚拟机载入class文件的时候不会将注解导入,因此运行时无法得到。如果不在注解中使用Retention指定,则默认的保留策略为CLASS。
RUNTIME:不仅保存到class文件,还会被虚拟机载入内存,因此在运行的时候可以使用这些注解。
5.2 使用反射获取注解:
如果一个注解的保留策略指定为RUNTIME,那么运行时就可以使用它,比如通过反射获取。下面是一个例子:
public class UseAnnotation {
/**
* @param args
*/
public static void main(String[] args) {
getAnno();
}
@MyAnno(str = "TT", val = 10)
public static void getAnno() {
// UseAnnotation use = new UseAnnotation();
Class<?> useClass = UseAnnotation.class;
try {
Method m = useClass.getMethod("getAnno");
System.out.println(m.getName());
<span style="background-color: rgb(255, 102, 102);">MyAnno anno = m.getAnnotation(MyAnno.class);</span>
System.out.println(anno.str() + " " + anno.val());
} catch (NoSuchMethodException ex) {
System.out.println("Method not found");
}
}
}
我们在静态方法getAnno定义的时候使用了MyAnno注解,指定两个成员的值为“TT”和10.然后在方法中获取注解,输出其成员的值。MyAnno代码如下:
<span style="background-color: rgb(255, 255, 0);">@Retention(RetentionPolicy.RUNTIME)</span>
public @interface MyAnno {
String str() default "test";
int val() default 900;
}
注意黄色部分将保留策略指定为RUNTIME,如果将这个去掉变成默认的保留策略,则程序将抛出NullPointerException,因为运行时候无法获取注解,红色背景的一行将抛出异常!
我们使用Method类型的对象m获取注解,这个getAnnotation由接口AnnotatedElement定义,该接口用于表示运行时被注解的元素,用于支持注解反射。类Method、Field、Constructor、Class、Package都实现了则个接口。
6. 几个重要问题
6.1 注解与继承
关于注解与继承,Java中定义了一个Inherited注解,源码如下:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {
}
这个注解只能用于注解注解,如果某个注解是可继承的(即注解定义中使用了@Interited),那么注解将会被继承。值得注意的是,被Inherited注解的注解,只影响类声明的注解,对其他诸如方法、构造器都没有影响(即使使用了)。如果MyAnno注解使用了Inherited注解,然后我们在类Father的声明中使用了注解MyAnno,其子类Son的声明将继承这个注解,可以在运行中通过反射获取。如果MyAnno没有使用Inherited注解,则不会继承。另外一点,注解的继承只存在于子类与父类,在接口与它的实现类之间不存在注解继承。
6.2 重复注解
Java 8引入了重复注解,即可以在同一个方法(或其他可以标注的元素)重复使用某个注解。要使用重复注解,必须经过以下2个步骤:
1)将要重复使用的注解用Repetable注解。格式如下:
<span style="background-color: rgb(255, 255, 0);">@Repeatable(Schedules.class)</span>
public @interface Schedule { String str();}
其中,括号中的Schedules.class用于容纳重复的注解,Schedules本身也是个注解,编译器编译的时候将重复的注解存在Schedules容器中。如果没有使用Repeatable注解,重复使用注解将导致编译错误。
2)定义作为容器的注解:
public @interface Schedules {
Schedule[] value();
}
作为容器的注解必须有一个value成员,它的类型是重复注解的数组。然后可以这么使用:
@Schedule(str = "TT")
@Schedule(str = "T")
重复的注解可以通过两种方式提取,示例代码如下:
Class<?> c = Reapt.class;
MyAnnos annos = c.getAnnotation(MyAnnos.class);
for (MyAnno a : annos.value()) {
a.value();
}
MyAnno[] aa = c.getAnnotationsByType(MyAnno.class);
for (MyAnno a : aa) {
a.val();
}
其中getAnnotationsByType将以数组的形式返回所有指定类型的注解。
版权所有,转载请注明出处。