注解(Annotation)就是一种标签,可以插入到源代码中,我们的编译器可以对他们进行逻辑判断,或者我们可以自己写一个工具方法来读取我们源代码中的注解信息,从而实现某种操作。需要申明一点,注解不会改变编译器的编译方式,也不会改变虚拟机指令执行的顺序,它更可以理解为是一种特殊的注释,本身不会起到任何作用,需要工具方法或者编译器本身读取注解的内容继而控制进行某种操作。本篇文章将从以下几点详细的介绍下Java注解的使用:
- 元数据和注解(Annotation)
- 按照参数个数分类注解(标记,单值,完整)
- 按照注解使用途径分类(标准,元注解,自定义)
- 自定义注解处理器完成读取注解内容的操作
一、元数据和注解
元数据(meta-data)就是指用来描述数据的数据,它往往是以标签的形式出现,主要用于描述代码块之间的联系。我们的注解就是一种元数据,根据它所起到的作用,我们可以大致将它分为以下三类:
- 编写文档:通过代码中标识的元数据生成文档
- 代码分析:通过代码中的元数据获取其中信息内容
- 编译检查:通过标记注解可以完成对代码块的检查,例如:@Override,用于检查格式
二、标准注解(系统自带)
在我们jdk的java.lang包中定义了三个注解,他们是:@Override,@Deprecated,@SuppressWarnnings。Override这个注解我们经常会使用到,在子类重写父类方法的时候就会使用到,他会帮助我们校验格式,确保我们正在定义的方法是在重写了父类的对应方法。Deprecated注解一般修饰在类或者方法之前,用于表示该方法或者类已经不再推荐使用了。SuppressWarnnings注解主要用于抑制编译器警告,具体的我们简单的演示下。
public class People {
public void sayHello(){
System.out.println("helo walker");
}
}
public class Student extends People {
/*@Override
public void sayHello(){
System.out.println("hello yam");
}这样是没有问题的*/
@Override
public void say(){
System.out.println("hello yam");
}/*如果你定义的方法不能重写父类某个方法,要么拼写错误,参数个数,方法名不一样等,编译抛出警告*/
}
我们需要注意的是,这里的override注解只能用于修饰方法,不能用于修饰类或者域。
public class Student extends People {
@Deprecated
public void say(){
System.out.println("hello yam");
}
}
//调用过时方法
public static void main(String[] args){
Student s = new Student();
s.say();
}
虽然编译时抛出了警告,但是程序依然可以正常的运行结束。此注解只是告知用户被标记的方法或者类已经不再推荐使用,但是你依然是可以使用的。之所以建议不再使用,一定是有了更好的取代物了,如果你一定要在你的项目中使用,等待新的jdk版本发布之后,很可能删除了这些方法或者类,可能会导致你的项目原先的一些方法或者类无法识别。
@SuppressWarnings("deprecation")
public static void main(String[] args){
Student s = new Student();
s.say();
}
例如,我们可以使用SuppressWarnings注解,阻止弹出过时警告。关于SuppressWarnings的参数主要有以下几种:
- deprecation:使用了不赞成使用的类或方法时的警告
- unchecked:执行了未检查的转换时的警告,例如当使用集合时没有用泛型 (Generics) 来指定集合保存的类型;
- fallthrough:当 Switch 程序块直接通往下一种情况而没有 Break 时的警告;
- path:在类路径、源文件路径等中有不存在的路径时的警告;
- serial:当在可序列化的类上缺少 serialVersionUID 定义时的警告;
- finally:任何 finally 子句不能正常完成时的警告;
- all:关于以上所有情况的警告。
三、元注解
元注解就是用来注解注解的注解。定义可能有点绕,其实元注解是一种注解,他可以加在一般的注解上用于限制该注解的使用范围,生命周期等。一般在自定义注解时候使用的多。在jdk的中java.lang.annotation包中定义了四个元注解:
- @Target:指定被修饰的注解的作用范围
- @Retention:指定了被修饰的注解的生命周期
- @Documented:指定了被修饰的注解是可以被例如Javadoc等工具文档化的
- @Inherited:指定了被修饰的注解修饰程序元素的时候是可以被子类继承的
我们首先看看@Target的使用:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
这是系统注解Override的定义源代码,我们看到Target注解中参数ElementType.METHOD表示该注解只能用于修饰方法。使用Target注解限定了Override的修饰范围只能使方法,不能是类或者域。Target还有一些其他的参数:
- CONSTRUCTOR:用于描述构造器
- FIELD:用于描述域
- LOCAL_VARIABLE:用于描述局部变量
- METHOD:用于描述方法
- PACKAGE:用于描述包
- PARAMETER:用于描述参数
- TYPE:用于描述类、接口(包括注解类型) 或enum声明
通过上述的参数我们可以在定义一个注解的时候限定他的作用范围。
下面看看Retention这个元注解,依然以注解Override为例,
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
我们看到Retention中使用了参数RetentionPolicy.SOURCE,这个参数表示该注解只在源代码中有效,进过编译之后将会被丢弃。还有一些其他参数:
- SOURCE:在源文件中有效(即源文件保留)
- CLASS:在class文件中有效(即class保留)
- RUNTIME:在运行时有效(即运行时保留)
SOURCE表示编译器编译之后的class文件中是不存在这一行注解代码的,CLASS范围表示编译器编译之后,注解代码存在于class文件中,但是jvm在加载此class文件的时候会自动忽略掉这一行注解代码。RUNTIME表示jvm加载class文件的时候会被读取到内存,也就是运行时保留。
接着使注解Documented,这是一个关于文档的元注解,被它注解的注解在注解其他方法或者类的时候可以被Javadoc等工具文档化,对于一般的注解,在Javadoc等工具文档化类或者方法的时候会丢弃注解内容,使用它就可以使得文档化的时候依然保存着注解代码。
//Test是一个被元注解Documented修饰
public class User {
@Test(value = 10,description = "do something")
public void test1() {
}
}
使用Javadoc生成API:
类User中的方法test1方法的头部是保留着注解的,如果是一般的注解则不会保留。
最后是元注解Inherited,我们知道如果一个普通的注解修饰了一个父类,那么他的子类是不能继承修饰父类的注解的。
@Deprecated
public class People {
public void sayHello(){
System.out.println("helo walker");
}
}
public class Student extends People {
public void say(){
System.out.println("hello yam");
}
}
我们可以看到,在父类people上使用了注解Deprecated,people类名上是有删除线的(粘贴到此处并没有显示)表示此类不推荐使用,但是我们可以看到在子类Student上是没有删除线的,也就是父类废弃了,子类依然是正常的。(注解不会被继承),但是如果我们希望子类能够继承父类的某些注解,那么只需要在定义该注解的时候使用我们的元注解Inherited修饰即可。
四、自定义注解
以上我们看到的标准注解,元注解都是jdk中定义好了的,如果我们想要自定义一个自己的注解就需要通过@interface来定义一个全新的注解。
//定义一个注解
public @interface myAnnotion {
}
使用@interface定义一个注解的时候,会自动继承java.lang.annotation.Annotation接口,以下是其中的一些方法:
boolean equals(Object obj);
int hashCode();
String toString();
Class<? extends Annotation> annotationType();
我们自定义的注解,除了多了个@符号,其他的和定义一个接口是一样的,所以这些方法我们不用实现。以上我们定义的是一个没有注解体的一个注解,像这样的注解我们叫做标记注解,这是表示一种标记,编译器根据某个类或方法是否具有此标记来判断是否要添加一些代码或做一定的检测操作。例如:@Override注解就是一个标记注解,如果某个方法前被修饰了此注解,编译器在编译时会找到父类,判断对应的方法是否完成了重写的格式。
下面声明了一个具有注解体的注解:
public @interface myAnnotion {
String name() default "";
int age();
}
我们说过,声明注解和声明接口很是类似,所以注解中的所有参数都必须以抽象方法的形式存在,例如上面一样。接下来我们看如何使用该注解:
@myAnnotion(name = "walker",age=10)
public class Test_ann {
public static void main(String[] args){
}
}
之前我们说过,注解本身不会起到任何作用,需要配合注解处理器才能发挥一定的作用,自己本身其实更像是一种特殊的注释。在上例中,我们可以在()中为注解的内部参数赋值,需要注意的是,注解的参数不允许为null,也就是在使用注解的时候,内部的每个参数都是必须要有数值的,要么在定义的时候给赋上默认值(使用default关键字),要么在()内显式的赋值。允许的注解参数类型有:
- 所有基本数据类型(int,float,boolean,byte,double,char,long,short)
- String类型
- Class类型
- enum类型
- Annotation类型
- 以上所有类型的数组
如果我们想要表示注解中某个参数不存在,该怎么办呢?比如我们用上述自定义的注解去修饰了一个People类,如果此人的age不知道,我们该如何赋值(参数的值不能为null)。我们往往用一些特殊值来标记某个参数不存在的情况,例如我们可以给age赋值-1表示此人年龄不详,在使用注解处理器读取的时候发现age等于-1,我们就知道此人年龄不详。往往字符串类型的参数用""表示参数不存在,整型类型参数使用负数表示参数不存在。
五、使用注解处理器响应注解
我们说过一个注解被定义出来之后,是不能完成任何作用的,如果没有注解处理器响应的注解和注释差不多。本小节我们看看如何定义一个注解处理器来对我们自定义的注解进行响应。还有一个前提是:我们的注解处理器实际上也是类,所以它只有在被加载到jvm中才能生效,但是如果我们的注解的生命周期范围到不了jvm的话,注解处理器也是没用的。
Java扩充了其反射机制,使得我们可以利用反射来获取注解信息。反射中的Class,Method,Constructor,Field,Package都继承了接口AnnotatedElement,这个接口主要有以下几个方法:
/*判断是否存在指定的注解*/
boolean isAnnotationPresent(Class<? extends Annotation> annotationClass)
/*获取指定的注解*/
<T extends Annotation> T getAnnotation(Class<T> annotationClass);
/*获取当前元素的所有注解*/
Annotation[] getAnnotations();
/*返回直接存在于此元素上的指定的注解,忽略继承,如果没有返回null*/
<T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass)
/*返回直接存在于此元素上的所有注解,忽略继承*/
Annotation[] getDeclaredAnnotations();
下面看一个注解的简单总和实例:
@Target(value={ElementType.FIELD})//修饰Filed的注解
@Retention(value = RetentionPolicy.RUNTIME ) //运行时保留
public @interface PName {
String name() default "";
}
@Target(value={ElementType.FIELD})//修饰Filed的注解
@Retention(value = RetentionPolicy.RUNTIME ) //运行时保留
public @interface PAge {
int age() default 0;
}
@Target(value={ElementType.METHOD})//修饰method的注解
@Retention(value = RetentionPolicy.RUNTIME ) //运行时保留
public @interface SayHello {
String content() default "hello";
}
public class People {
@PName(name = "people")
private String name;
@PAge(age = 20)
private int age;
@SayHello(content = "hello people")
public void sayHello(){
System.out.println("hello people");
}
}
public static void main(String[] args) throws NoSuchMethodException {
//获取people类中所有注解信息
Field[] fields = People.class.getDeclaredFields();
for(Field f : fields){
//遍历每个属性
if(f.isAnnotationPresent(PName.class)){
PName pn = f.getAnnotation(PName.class);
System.out.println(pn.name());
}else{
PAge pa = f.getAnnotation(PAge.class);
System.out.println(pa.age());
}
}
Method md = People.class.getMethod("sayHello");
SayHello sh = md.getAnnotation(SayHello.class);
System.out.println(sh.content());
}
上述的代码完成了将people类中所有注解信息全部获取打印的工作。这个例子可能不能准确的描述注解在我们程序中的作用(起码注解不会用来干这个),但是在一方面演示了定义到使用注解的过程,希望对大家在项目中实际使用有所启发。
最后,本篇文章结束了,望大家多多留言交流,相互学习。