概述
Java提供了一种可以在源文件中嵌入附加信息的功能(从JDK5开始提供的)。这些信息称为注解(annotation),它们不会改变程序的运行,但是这些信息可以在开发和部署时期让很多工具使用。例如:注解可以由源代码生成器、编译器或者部署工具处理。虽然元数据(metadata)这个术语也用来指示这个功能,但是术语“注解”的描述显得更为恰当,并且更为常用。
牛刀小试
我们编写一个标准的Person类,并且重写Object类的toString()方法:
此处,@Override就是一个注解。正如概述中所说的,@Override这个注解不会影响程序的运行,只是告诉编译器toString()方法是Person类重写的父类方法,在本例中,指的是Object类的toString()方法。另外,本例中的@Override注解是简单的标记注解(不带形参的注解),下面我们给出的MyAnnotation就是一个带有形参的注解。关于注解,我们需要了解下图的知识:
注解的基本知识
注解是通过基于interface的机制创建的,如下图所示:
上述语句声明了一个名为MyAnnotation的注解。我们需要注意两点:
1)interface关键字前面有一个@,它告诉编译器此处声明了一个注解
2)我们需要注意两个成员str()和val(),所有的注解都仅由方法组成。但是不能提供方法体,而是有JAVA来实现这些方法,这些方法实际上就像成员变量一样。
所有的注解都自动继承了java.lang.annotation.Annotation接口,所以,注解不能包含extends子句。
起初,注解仅用来标注声明。这样使用它时,任何类型的声明都可以有一个与之对应的注解。例如,类、方法、变量、形式参数、enum都可以有注解。甚至注解本身也可以带有注解。在上述所有情况下,注解都位于其声明之前。从JDK8开始,也可以对类型用法(type use)进行注解,如强制类型转换或者方法返回类型。
使用注解后,需要向其成员传递值。例如:我们在一个方法上使用我们自己写的MyAnnotation注解:
此处,MyAnnotation注解与test()方法链接,注解名前置了@,后面跟成员初始值的括号列表,为了给成员提供值,需要向成员赋值。因此,本例中,“注解示例”字符串赋值给了str成员,整数100赋值给了val成员。给注解成员赋值时只使用成员的名字,后面不跟括号,和给变量赋值一样。
注解的保留策略
我们知道,注解是一种标记,那么,该标记会被保留到什么程度?保留策略就是用来决定在什么位置丢弃注解。JAVA定义了三种策略,这三种策略被定义在一个枚举类型中:
1)使用SOURCE保留策略的注解,只在源文件中保留,编译期间会被丢弃。
2)使用CLASS保留策略的注解,在编译时被存储到.class文件中,但是在运行时不能通过JVM得到这些注解。
3)使用RUNTIME保留策略的注解,不仅被保留在.class文件中,而且在运行时能够通过JVM得到这些注解。因此,RUNTIME提供了永久的注解。
4)需要注意的是:局部变量声明的注解不能存储在.class文件中
保留策略是通过java的内置注解@Retention指定的,它的一般形式为:@Retention(policy),其中policy必须为RetentionPolicy枚举的常量,注解的默认保留策略是CLASS,下面我们给我们写的MyAnnotation注解指定保留策略:
注解和反射
当一个注解的保留策略是RUNTIME的时候,任何程序都可以在运行的时候通过反射来查询注解信息。关于反射,我们不在此处讨论,我们只需要知道反射可以在运行的时候获取类的相关信息。我们可以用反射包下的AnnotatedElement接口来获取注解的元素相关信息,Constructor、Field、Method、Package和Class都实现了AnnotatedElement接口,因此,我们可以通过反射拿到之前写的test方法(是一个Method类型的对象),然后获取test方法上的注解信息:
注解成员的默认值
可以为注解成员提供默认值,应用注解时,如果没有为注解成员指定值,就会使用默认值,我们通过使用default子句来添加默认值:
上面的程序为str提供默认值“默认值”,为val提供默认值1024。这样一来,我们在使用@MyAnnotation的时候,可以不需要为这两个成员指定值了,当然也可以指定一个或者两个,因此可以通过以下4种方式使用@MyAnnotation:
标记注解和单成员注解
标记注解:不包含成员的注解叫做标记注解。
单成员注解:包含一个成员的注解叫做单成员注解。关于单成员注解,我们需要掌握两点:
1)成员名建议统一写成value
2)党成员名是value的时候,应用注解时,可以直接指定值,不需要指定成员的名称。
内置注解
JAVA提供了很多内置注解,大部分都是专用注解,但是有9个使用于一般情况。这9个中,有4个来自java.lang.annotation包,5个来自java.lang包。
1)@Retention
@Retention只能注解其他注解,指定注解的保留策略。
2)@Documented
@Documented只能注解其他注解,告诉工具注解被文档化。
3)@Target
@Target只能注解其他注解,用于指定应用该注解的声明类型。@Target注解只有一个参数,该参数必须是ElementType枚举的常量,ElementType定义的常量如下:
在@Target注解中可以指定这些值中的一个或者多个。如果指定多个值的话,必须由大括号{}包围起来:
4)@Inherited
@Inherited是标记注解,只能用于另一个注解的声明,另外@Inherited注解只影响用于类声明的注解。@Inherited注解会导致父类的直接被子类继承。也就是说,当我们通过反射查询子类注解的时候,如果子类找不到,就查询父类,如果父类中存在并且使用了@Inherited,就返回父类的注解。
5)@Override
@Override是标记注解,只用于方法,说明该方法是重写了父类的方法,这个在牛刀小试的时候就用到了。
6)@Deprecated
@Deprecated是标记注解,指定声明已经过时,被新的声明代替。
7)@FunctionalInterface
@FunctionalInterface是JDK8新增的注解,用于接口,指定该接口是一个函数式接口,函数式接口仅仅包含一个抽象方法,由lambda表达式使用。
8)@SafeVarargs
@SafeVarargs是标记注解,只用于方法和构造方法,指定没有发生于可变长度参数相关的不安全操作。
9)@SuppressWarnings
@SuppressWarnings用于抑制一个或者多个编译器可能报错的警告。
类型注解、参数注解和重复注解
前面我们提到过,注解只能用于声明,JDK8开始,JAVA增加了可以使用注解的地方,在能够使用类型的大多数地方都能够使用注解。例如,可以注解方法的返回值类型,方法内this的类型,强制转换,数组级别,被继承的类以及throws子句。还可以注解泛型,包括泛型类型参数和泛型类型参数。类型注解和参数注解允许工具检查javac本身不进行的额外检查,从而帮助避免错误。
类型注解必须是@Target为ElementType.TYPE_USE的注解,参数注解必须是@Target为ElementType.TYPE_PARAMETER的注解,他们使得注解可以应用在任何地方,例如:
创建类实例:
new @Interned MyObject();
强制类型转换:
myString = (@NonNull String) str;
implements 语句中:
class UnmodifiableList implements @Readonly List { ... }
throw exception声明:
void monitorTemperature() throws @Critical TemperatureException { ... }
需要注意的是,类型注解和参数注解只是语法,并不会影响运行,编译成.class文件不包含类型注解和参数注解。
JDK8还新增了重复注解,被@Repeatable注解的注解叫做重复注解,重复注解允许在同一声明类型(类,属性,或方法)上多次使用同一个注解。实际上,重复注解不是一个语言上的改变,只是编译器层面的改动,技术层面仍然是一样的,我们可以通过其他的技术手段来替代JDK8的重复注解,只是JDK8的重复注解在语义上更加优秀,并且使得代码可读性高。例如,我们想通过注解的方式来指定权限:
JDK8之前的方案(非重复注解方案):
JDK8的重复注解方案: