注解就是java程序中一种特殊的注释,不要把注解想的那么复杂,它就是注释而已,注解本身不会对程序的代码逻辑造成任何影响。
在学java类与对象时,有关方法覆写那里,你可能会看到下面这样的代码:
class A extends B{
@Override
public void run(){}
}
这可能是我们最早接触到的一个注解。对,在这段代码里,@Override就是一个注解。它用来让编译器检查run()方法是否正确的进行了覆写,如果没有那就报错。
但如果我们不写@Override对代码有影响么?其实也没有。
它更多的是起到一个标注作用。比如你看到这段代码,立马就会知道,“哦,run()方法是一个覆写方法。”不仅如此,它还提供编译检查,比如@Override。
仅仅是编译检查这一点,java注解就已经与javadoc大不相同。javadoc纯粹就是一个帮助说明文档,但注解不仅仅是说明文档,它的功能大致有以下几个方面:
- 编译检查
- 生成帮助文档
- 使代码可读性更强
- 运行时获得注解信息(借助java反射机制)
接触一个新知识,一定要先搞清楚它是用来干什么的,现在你已经了解了java注解的主要用途,接下来再来慢慢的拓展更多的内容。
一、Java内置的注解
注解也是一种class,java.lang.annotation包中有一个Annotation接口,任何一个注解都要实现这个接口。这意味着我们完全可以自定义一个注解,但在此之前,需要先对java内置的注解做一个了解,同时通过介绍这些java内置的注解,理解java注解的语法和结构。
java中内置的注解就有十个,其中三个在java.lang包中,这意味着这三个注解我们是可以直接拿来用的,而不需要导入其它的包。
这三个注解是:
- @Override :检查该方法是否是覆写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。
- @Deprecated :标记过时方法。如果使用该方法,会报编译警告。
- @SuppressWarnings :指示编译器去忽略注解中声明的警告。
另外还有七个注解在java.long.annotation包下,它们分别是:
- @Retention :用于定义注解的生命周期。
- @Documented : 标记这些注解是否包含在用户文档中。
- @Target :定义注解能够被应用于源码的哪些位置。
- @Inherited :定义子类是否可以继承父类定义的注解。
- @Repeatable :定义注解是否可以在同一个声明上使用多次。
注意到这五个注解有一个共同点,即它们的作用对象也都是注解。这意味着它们可以用来修饰其它注解,因此这些注解也被称为元注解。
除此之外,还有两个注解:
- @SafeVarargs : 忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。
- @FunctionalInterface :标识一个匿名函数或函数式接口。
这两个都是Java 7或者Java 8才新增的,并不是太常用,同样不太常用的还有 @Repeatable。
接下来,以这些内置注解为例,来看一看java注解是怎样实现的吧。
二、定义一个注解
在详细探讨内置注解之前,有必要首先学会怎样定义一个注解。
如下:
@Target(ElemType.Type)
@Retention(RetentionPolicy.RUNTIME)
@interface MyAnnotation{
}
是不是感觉上面的代码很奇怪?我们说注解也是一种class,但它既不是用class 定义,也不是用 interface定义,而是用 @interface 定义。在这段代码中,我们定义了一个注解,并命名为 MyAnnotation 。注意 @interface就是定义注解的,且只能用它来定义。只有用 @interface 定义注解,才意味着它实现了java.lang.annotation.Annotation接口。
我们说,元注解是用来修饰注解的,即我们自己定义的注解。如果我们没有自定义注解,那自然用不上元注解。元注解有5个,具体是哪五个上面已经给出。在如上例子中,MyAnnotation 被两个元注解修饰,分别是 @Target 和 @Retention。
这两个元注解很重要,因为它们刚好配合 @interface 定义了一个完整的注解。什么意思呢?但凡你定义了一个注解,就必须用 @Target 和 @Retention 修饰。即使你没有明确指定,编译期也会默认加上这两个元注解。为什么会这样呢?这其实与注解的构成有关。
一个注解有三个组成部分,分别是 Annotation接口、ElementType以及RetentionPolicy。
Annotation接口好说,只要是用 @interface 定义的注解都会实现这个接口。
ElementType是指定注解能够被应用于源码的哪些位置的,这显然与 @Target 的作用是一致的。ElementType是一个枚举类,它的取值有多个,具体的取值可参照如下源码:
package java.lang.annotation;
public enum ElementType {
TYPE, /* 类、接口(包括注释类型)或枚举声明 */
FIELD, /* 字段声明(包括枚举常量) */
METHOD, /* 方法声明 */
PARAMETER, /* 参数声明 */
CONSTRUCTOR, /* 构造方法声明 */
LOCAL_VARIABLE, /* 局部变量声明 */
ANNOTATION_TYPE, /* 注释类型声明 */
PACKAGE /* 包声明 */
}
一个注解可以指定多个ElementType。这里注意如果要指定多个ElementType的话需要向 @Target中传入一个 ElementType[] 类型。
可以参考 @Target 的源码:
package java.lang.annotation;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
ElementType[] value();
}
RetentionPolicy也很好理解,它用来指定注解的生命周期,或者说,注解的执行策略。比如 @Override 仅在编译期有效,这意味着该注解在编译期之后就没有任何作用了。这就是注解的其中一种策略,而这些策略完全由 @Retention 指定。
可以参看@Override的源码:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
RetentionPolicy也是一个枚举类,它有三个取值,源码参考如下:
package java.lang.annotation;
public enum RetentionPolicy {
SOURCE, /* Annotation信息仅存在于编译器处理期间,编译器处理完之后就没有该Annotation信息了 */
CLASS, /* 编译器将Annotation存储于类对应的.class文件中。默认行为 */
RUNTIME /* 编译器将Annotation存储于class文件中,并且可由JVM读入 */
}
接着给出@Retention的源码提供参考:
package java.lang.annotation;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
RetentionPolicy value();
}
注意一个注解只能有一个RetentionPolicy。
结合如上,每一个注解的构成必须满足如下:
- 实现了Annotation接口
- 与一个或多个ElementType相关联
- 与一个RetentionPolicy相关联
因此,如果你需要自定义注解,那么请务必不要忘了指定 @Target 和 @Retention。即使你确实没有指定,那么 @Target 默认将应用到全部类型上,而 @Retention 将默认使用RetentionPolicy.CLASS策略。
@Target 与 @Retention说完了,再来说一说剩下的三个元注解。
#1 @Documented
@Documented用于指定注解是否可以出现在javadoc中,它默认是不出现的,如果想要它出现,那么在自定义的注解上添加上该注解即可。
不太清楚javadoc的,只需知道javadoc是指将代码注释文档化,即在归档时注释也会被编译。
它的源码如下:
package java.lang.annotation;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {
}
#2 @Inherited
@Inherited 指定子类是否可以继承父类定义的注解。在默认情况下是不被继承的。另外需要知道,继承是只有 class 和 interface 才有的概念,因此它只针对@Target(ElementType.TYPE)类型的注解有效,且只针对于class的继承,对interface的继承无效。
它的源码如下:
package java.lang.annotation;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {
}
接下来给出一个例子:
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@interface Inheritable{
}
@Inheritable
class InheritableFather
{
public InheritableFather() {
System.out.println("InheritableFather:"+InheritableFather.class.isAnnotationPresent(Inheritable.class));
}
}
public class InheritableSon extends InheritableFather
{
public InheritableSon() {
super();
System.out.println("InheritableSon:"+InheritableSon.class.isAnnotationPresent(Inheritable.class));
}
public static void main(String[] args)
{
InheritableSon is = new InheritableSon();
}
}
运行结果:
InheritableFather:true
InheritableSon:true
在这个代码中,我们自定义了一个注解 Inheritable ,它被修饰为具有继承性。为了验证该继承性,我们特意定义了一个父类InheritableFather和其子类InheritableSon。
InheritableFather类持有Inheritable注解,且Inheritable注解被 @Inherited修饰,因此InheritableFather类的子类也将持有Inheritable注解。而运行结果显示两个结果均为true也刚好印证了我们的观点。
在输出运行结果那里,对于isAnnotationPresent()方法可能有些人会不太了解。这里多说一下。我们说,注解也是一种class,既然是class,那么自然就可以借助java的反射机制得到该注解的信息。另外,要知道,对于RetentionPolicy为SOURCE和CLASS策略的注解来说,前者在编译期就会被丢弃,而后者只会被保存进class文件中,不会加载进JVM内存中。因此这里显然是只针对于RUNTIME类型的注解的讨论。
java提供的使用反射机制读取注解的方法有:
判断某个注解是否存在于Class、Field、Method或Constructor:
- Class.isAnnotationPresent(Class)
- Field.isAnnotationPresent(Class)
- Method.isAnnotationPresent(Class)
- Constructor.isAnnotationPresent(Class)
好了,作为对照,如果将上面的示例代码中Inheritable注解的 @Inherited 去掉,那么运行结果将如下:
InheritableFather:true
InheritableSon:false
#3 @Repeateble
该注解不太常用,用途也很简单,这里不再多说。
三、java.lang包下的内置注解
元注解已经介绍完了,注解的组成与语法以及如何定义一个注解也都已讲解,不知道我说清楚了没有。接下来把java.lang包下的三个注解再做一个简短的说明。
java.lang包下有三个注解,分别是@Override、@Deprecated和@SuppressWarnings。
其中,@Override已经说过,且接触的比较多,这里不再给出示例。
#1 @Deprecated
定义源代码:
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface Deprecated {
}
可以看到,@Deprecated并没有指定@Target,因此它是可以作用于代码的任何地方的。被@Deprecated标志的部分不再被建议使用,在Eclipse中即表现为相关的代码被打上了删除线。但不再被建议使用,其实还是可以使用,只是你最好不要这么用。因为可能用了就会出错。
我想着示例代码就不再写了吧,毕竟很好理解。
#2 @SuppressWarnings
源代码定义如下:
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
String[] value();
}
@SuppressWarnings主要起到一个编译检查的作用,它使对它所标注的内容的某些警告保持静默。
注意使用该注解需要给出参数,且参数应该为字符数组类型。因为定义了String[] value。
看一个示例:
import java.util.Date;
public class SuppressWarningTest {
@SuppressWarnings(value={"deprecation"})
public static void doSomething(){
Date date = new Date(113, 8, 26);
System.out.println(date);
}
public static void main(String[] args) {
doSomething();
}
}
这个例子编译之后无任何问题。但如果你把 @SuppressWarnings 去掉的话,在编译期会给出一个警告。原因是Data类在java中已经是不再被建议使用的类,即已经被 @Deprecated 了,但因为 @SuppressWarnings 的缘故,编译器对该警告保持了默许。
补充,@SuppressWarnings 常用的关键字表格:
deprecation -- 使用了不赞成使用的类或方法时的警告
unchecked -- 执行了未检查的转换时的警告,例如当使用集合时没有用泛型 (Generics) 来指定集合保存的类型。
fallthrough -- 当 Switch 程序块直接通往下一种情况而没有 Break 时的警告。
path -- 在类路径、源文件路径等中有不存在的路径时的警告。
serial -- 当在可序列化的类上缺少 serialVersionUID 定义时的警告。
finally -- 任何 finally 子句不能正常完成时的警告。
all -- 关于以上所有情况的警告。
四、给注解配置参数
我们在定义一个注解时,还可以给它配置参数。但要保证配置的参数必须是常量,因为注解要在定义后就能确定每个参数的值。因此参数的类型可以是:
- 基本类型
- String
- 枚举类型
- 上述三种类型的数组类型
参数类型可以有默认值,即如果你给注解配置了参数,但没有去用它,那么该参数将自动使用默认值。
另外,大部分注解都会有一个名为value的配置参数。
示例:
@Retention(RetentionPolicy.RUNTIME)
@Target(ELementType.FIELD)
public @interface Range {
int min() default 0;
int max() default 255;
}
在如上例子中,我们定义了一个Range注解,并给它配置有min和max参数,min和max参数的默认值分别是0和255。这里要提一点,我们定义的注解大都是RUNTIME注解,为什么呢?因为如果是其它策略的注解,那么定义参数什么的就没有意义了,因为不会被加载进JVM中,自然也就无法用反射来得到任何信息。
public class Text {
@Range(min = 0,max = 999)
public int n;
@Range(max = 999) //min和value均使用默认值
public int x;
}
这个地方有些人可能会困惑,觉得@Range注解限制了字段n或者x的值,比如以n为例,如果我们把n赋值为100000,远远超出了@Range注解中参数所指定的min-max之间的范围,那么程序会报错么?
肯定不会,在最开始我就说了,注解不会对代码逻辑造成任何影响,这里的n,只要你对它的赋值不超出int范围那就毫无问题,而与它的注解指定的参数无任何关系。那么既然毫无关系,我们还偏要写上这个参数min和max,岂不是显得毫无意义了?
当然不是,首先,注解给代码调用者一个清晰的认识,它在看到这段代码时就立马会知道,我们想要的是一个0-999之间的n。甚至我们完全可以写一个辅助函数来实现这个限制功能,如果n不是0-999之间的值就会抛出错误。
其次,即便其他人在代码运行期得到了不知名的错误,通过反射机制,他也能迅速的了解到该注解的用意然后来规避这个错误。
参考:
https://www.runoob.com/w3cnote/java-annotation.html