Java 注解

对于 Java 的注解,其实前面我们已经接触过了,在对接口或者抽象类的方法进行重写的时候,我们会在重写的方法的前面加上 @Override 这个注解,注意这个是注解不是注释,注释是完全被编译器忽略的东西,而注解并不会被忽视,是会被编译器处理的。这个注解的作用就是让编译器帮我们检查我们重写的方法是否正确。

按照 Java 中一切皆为对象来看,注解也自然是对象了(其实准确的说,注解本质上是一个接口,继承于 java.lang.annotation.Annotation ),下面的代码就是 @Override 注解的声明:

1
2
3
4
5
6
7
package java.lang;
import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

注解的作用

注解在 Java 中一般拥有三个用途:

  1. 用来编写文档

  2. 用来编译检查

  3. 用来代码分析

这些用途我们一点一点的说:

用来编写文档

记得我最开始在写 Java 的基础笔记里面,提到了 JDK 里面是拥有一个叫作 javadoc.exe 的程序,这个程序的作用就是从 Java 代码中提取注释,然后整合成文档。我们同样可以在我们自己的代码中对这些进行尝试,用注解标识自己的代码,然后使用 javadoc.exe 程序将这些注解进行提取,生成自己代码的一个功能文档。

首先是利用注解对代码进行标注,下面代码写了一个类中,一个 main 方法和一个将字符串自身累加一次的 PlusOnce 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package Java.test;
import java.io.IOException;

/**
* <p>This class is discribe the test.
* <p>First of all, this class is the best class in the world.
* <ul><li>The
* <li>best
* <li>class!</ul>
*
* @author Xorex
* @version 1.0
* @since JDK 1.8
*/
public class test {

/**
* This method is the main method.
* And it only output a number 10000;
* @author Xorex
* @param args Parameter is the orders.
*/
public static void main(String[] args) {
int a=10000;
System.out.println(a);
}

/**
* This method is the PlusOnce method:
* So what is the best of the String plus String?
* Just plus it!
* @author Xorex
* @param a Input a String a.
* @return The double lenght of the a,to be a+a .
* @exception IOException It will throw IOException
* @see IOException
* @deprecated It is not good yet.
*/
public String PlusOnce(String a) throws IOException {
return a+a;
}

}

使用 Java 中自带的一些注解对类和方法进行了一个比较基本的描述,需要注意的是,Java 中使用的注释为单行注释 // 多行注释 /**/ ,但是注解与他们不同,使用的是这样: /** */ 要比多行注释多了一个星。我们的注解就写在里面。里面的注解描述的都是很基本的信息,然后使用命令 javadoc test.java 成功生成了一堆网页文件,打开 index.html 就可以看到生成的文档了。

点开之后里面就有很多东西了,我们所有的标注都被提取出来,变成了一个文档。

用来编译检查

比如以前使用的的 @Override 来检查是否重写方法正确,使用 @SuppressWarnings 来让编译器忽略此处代码产生的警告信息。

注解也是拥有参数的,比如 @SuppressWarnings 就拥有一个类型为 String 的参数,用来接收数据,从而命令编译器忽略哪一种类型的警告,下面是输入参数对应的各种效果:

  • all 压制所有的警告
  • deprecation 压制使用不赞成类和方法警告
  • unused 压制未使用变量警告
  • unchecked 压制未检查转化警告
  • path 压制路径不存在警告

使用方法就是在语句前加上 @SuppressWarnings("all");

用来代码分析

这是对于我们一个普通的开发者来说,注解的用途就是来帮助我们进行分析代码。我们可以自己写一个用来处理代码的工具比如一个测试代码是否有运行错误的工具。那么这个工具是如何知道应该运行哪些代码,应该以什么方式运行代码呢?这就用到了注解,我们利用注解对代码进行标注,然后代码分析工具读取我们的标注,按照标注的注解来决定如何处置这些代码,这是我们一般自定义注解的用途。

注解的组成

注解的声明

Java 的注解组成来看,先看看上面我们介绍过的注解 @SuppressWarnings 的定义代码:

1
2
3
4
5
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, MODULE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
String[] value(); //这东西看起来像一个抽象方法,但是实际上是个属性
}

首先声明使用方法自然是 public 类型的 @interface + 注解名 来声明出来一个拥有名字的注解,注解内部用来声明注解需要输入的参数,比如上面的参数输入就是一个字符串数组,参数名为后面设置的 value,可以在参数声明后面加上默认值,作用就是在使用这个注解的时候,没有输入此参数的时候,这个参数会接受默认值:

1
2
String name() default "Xorex";
int id() default 100;

元注解

上面除了像声明了一个接口一样声明了一个注解的代码,还有一些注解与注解定义的注解,比如上面的 @Target@Retention ,他们被叫作元注解,下面是在 Java 中内置的一些元注解

1. @Target

Target 注解用来定义声明的这个 Annotation 能够被作用的位置,是定义注解时必须加上的元注解。

Target 接受的参数是一个枚举类型的数组,叫作 ElementType[] 这个枚举类型里面拥有定义好的元素,分别对应不同的作用位置:

  • 类或接口:ElementType.TYPE
  • 字段:ElementType.FIELD
  • 方法:ElementType.METHOD
  • 构造方法:ElementType.CONSTRUCTOR
  • 方法参数:ElementType.PARAMETER

如果需要定义多个可作用位置,只需要把他们构造成一个数组即可。

2. @Retention

Retention n. 保留,扣留,记忆力。这个元注解用来定义 Annotation 的生命周期。

Retention 接受的参数同样也是一个枚举类型的单个常量,叫作 RetentionPolicy ,这个枚举类有一下的选项:

  • RetentionPolicy.SOURCE 表示此注解的生命周期可以维持到源码中存在,编译完就会被丢掉。
  • RetentionPolicy.CLASS 表示此注解的生命周期可以维持到编译文件 .class 中存在,会被留在编译文件里。
  • RetentionPolicy.RUNTIME 表示此注解的生命周期可以维持到运行时,会被 JVM 读取并处理。

如果不加入这条注解,那么默认生命周期为 RetentionPolicy.CLASS 级别,但是考虑到一般来说,我们自定义的注解作用的生命周期都是运行时,所以需要加上 RetentionPolicy.RUNTIME 这一条设置。

3. @Repeatable

用于表示我们自定义的 Annotation 是否被允许重复注解一个元素。

使用方法就是在定义 Annotation 的时候,用此 Annotation 经过编译之后的文件名作为元注解 @Repeatable 的参数,就可以设置此 Annotation 在使用的时候,可以重复作用与同一个元素了。

不过感觉这个东西用处不大啊,如果要传入多条参数的话,直接修改注解定义的参数即可了。

4. @Inherited

Inherited adj. 遗传的,继承的。 这个用来表示对于某 Annotation,注解了父类之后,子类是否会继承这个注解。需要注意的是这个元注解只能作用与同时拥有 Target(ElementType.TYPE) 元注解的 Annotation,因为有 @Inherited 的前提是有继承关系,因为只有类和接口能被继承,所以才有这样的硬性规定。

如果某 Annotation 允许子类从父类或接口那里继承与自己的话,只需要在定义 Annotation 的前面加上元注解 @Inherited; 即可,不需要填写任何参数。

5. @Documented

在使用 javadoc.exe 进行抽取一个 java 文件里面的类的信息的时候,自定义的注解一般不会被提取并显示在生成的文档中,也就是说,我们在 javadoc.exe 看一个类的信息的时候,如果使用了自定义的注解,那么默认这些是在类信息里面看不到的,我们不知道这个类有没有使用某种特定的注解。

如何让使用某个我们自定义注解的类,它提取文档里面有此类使用了我们定义的注解的信息呢?

答案就是在定义这个注解的时候,在这个注解前面加上元注解 @Documented 即可。这样凡是使用了这个注解的了类,在自己的文档里面就会有体现使用我们定义注解的信息了。

定义 Annotation

综上,我们可以总结出来定义一个 Annotation 的方法:

首先使用 @interface 定义一个拥有名字的注解,然后往此注解中添加参数,建议所有的参数就加上默认值,最后使用元注解对自己定义的 Annotation 进行修饰。

利用自定义的注解

获取被标注的注解的实例

实际上利用自定义的注解主要是利用 RUNTIME 类型的注解,而要按照标注的注解处理程序,就需要读取到这些注解,并分析注解里面的属性值。注解是依托于被标注物存在的,所以我们读取这些标注的方法就是依靠被标注物的反射机制获取的。

对于一个类来说,可以通过使用反射获取一个保存了它 所有信息 的 Class 类的实例,既然是所有信息,那么也包括它的注解,而获取注解的方法就是使用 Class 类中的方法。同理,对于方法,字段,构造器的反射出来的实例也有用来处理注解的方法,比如判断某个注解是否标注(参数里的 Class 为注解的 Class 类实例(本质是接口嘛)):

  • Class.isAnnotationPresent(Class)
  • Field.isAnnotationPresent(Class)
  • Method.isAnnotationPresent(Class)
  • Constructor.isAnnotationPresent(Class)

举个例子,判断类 Tempest 中的方法 Xorex() 中是否有注解 @Override :

1
Tempest.class.getMethod("Xorex").isAnnotationPresent(Override.class);

当然还可以直接获取注解的 Class 实例,使用的是下面,返回的是标记此元素注解的实例(包括具体的属性的):

  • Class.getAnnotation(Class)
  • Field.getAnnotation(Class)
  • Method.getAnnotation(Class)
  • Constructor.getAnnotation(Class)

或者直接返回所有的注解,返回类型是 Annotation[]:

  • Class.getAnnotations()
  • Field.getAnnotations()
  • Method.getAnnotations()
  • Constructor.getAnnotations()

所以利用反射机制获取注解实例的方法有两种,一种是先判断有没有,然后再获取,另外一种是直接获取,如果没有就会返回 null 注意处理这种返回情况就可以了。


这里有一个比较特殊的注解获取,因为能被注解标注的,除了上面几个意外,还有方法的参数们。而参数们又对应着不止一个注解,所以 Method.getParameterAnnotations() 返回的是一个 Annotation[][] 的二维数组,第一维对应的是不同的参数,第二维对应的是参数的不同注解。

利用注解信息处理代码

现在我们知道了如何获取被标注代码中的注解信息,那么我们就可以根据这些注解信息的不同,采用不同的方式处理这些代码,比如我们可以写一个测试框架,对于输入的一个类,获取所有的方法,然后对于每一个方法,判断是否存在注解 @Test ,若存在,则利用反射执行这个方法,判断是否抛出异常从而记录日志。如果没有注解 @Test ,那么就不测试,最后输出测试日志。

这就是注解的一个用途,帮助程序分析程序。