Java中提供了注解(Annotation)功能,该功能可用于类、构造方法、成员变量、方法、参数等的声明中。该功能并不影响程序的运行,但是会对编译器警告等辅助工具产生影响。本文将介绍注解的概念及如何使用注解,最后将通过两个案例将所学的知识运用到实际开发中。
一、什么是注解?
1、注解的定义
注解(Annotation),也叫元数据。一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释。
所谓注解,可以看作是对 一个 类/方法 的一个扩展的模版,每个 类/方法 按照注解类中的规则,来为 类/方法 注解不同的参数,在用到的地方可以得到不同的 类/方法 中注解的各种参数与值。其实说白就是代码里的特殊标志,这些标志可以在编译,类加载,运行时被读取,并执行相应的处理,以便于其他工具补充信息或者进行部署。
2、注解的作用
注解主要有以下三种功能:
编写文档:通过代码里标识的元数据生成文档
编译检查:通过代码里标识的元数据让编译器能够实现基本的编译检查
代码分析:通过代码里标识的元数据对代码进行分析
接下来的内容将体现出注解在这三个方面的作用。
二、Java中常用的注解
1、@Override
该注解的作用是对覆盖超类中方法的方法进行标记,如果被标记的方法并没有实际覆盖超类中的方法,则编译器会发出错误警告。
下面通过举例来说明@Override
注解的用法:
除此之外,在接口的实现类中每个方法都要加上@Override
注解,这里不再进行举例说明。
2、@Deprecated
该注解的作用是对不应该再使用的方法添加注解,当编程人员使用这些方法时,将会在编译时显示提示信息,它与javadoc
里的@deprecated
标记有相同的功能,用@Deprecated
的示例代码示例如下:
在调用加上@Deprecated
注解的方法会出现以下情况:
3、@SuppressWarnings
该注解的作用是指示编译器去忽略注解中声明的警告,其参数有:
deprecation
:使用了过时的类或方法时的警告unchecked
:执行了未检查的转换时的警告fallthrough
:当switch
程序块直接通往下一种情况而没有break
时的警告path
:在类路径、源文件路径等中有不存在的路径时的警告serial
:当在可序列化的类上缺少serialVersionUID
定义时的警告finally
:任何finally
子句不能正常完成时的警告all
:关于以上所有情况的警告
下面将通过一个例子来说明@SuppressWarnings
注解的用法:
如上图所示,编译器会对代码中可能出现异常或无意义的代码进行警告,如果想忽略这些警告,可以在语句、方法或类上使用@SuppressWarnings
注解忽略这些警告。
如果想要更加简单,可以直接在方法或类上添加@SuppressWarnings
注解。
(1)在方法上添加@SuppressWarnings
注解
(2)在类上添加@SuppressWarnings
注解
三、注解的实现原理
既然Java自带了一些注解,那么我们可不可以自定义注解呢?答案是肯定的。不过在自定义注解之前,我们需要了解注解的实现原理,这样才能更好的理解和掌握自定义注解的方法。
1、Annotation
首先,我们查看一下Java中自带的注解是如何实现的,就以@Override
注解为例,该注解的定义如下:
按照上面的格式,我们自己也来写一个注解:
1 | package my.study.annotation.anno; |
之后我们用javac
对MyAnnotation.java
进行编译,生成MyAnnotation.class
字节码文件,然后再用javap
对MyAnnotation.class
进行反编译,结果如下:
通过反编译我们终于发现了注解的奥秘,注解本质上就是一个接口,并且这个接口需要继承Annotation
接口,接下来就去看看该接口究竟是怎样的,这里给出API文档中关于Annotation
接口的说明:
通过API文档的说明,我们可以知道所有的注解接口类都要继承Annotation
接口,使用@interface
定义注解时即表示继承了 java.lang.annotation.Annotation
接口。
注意:注解的实现和我们通常实现接口的方法不同。Annotation 接口的实现细节都由编译器完成。通过 @interface
定义注解后,该注解不能继承其他的注解或接口。
下面给出Annotation
的整体结构图:
从图中可以看出对于每一个Annotation
对象都有唯一的RetentionPolicy
属性,对于每一个Annotation
可以有若干个ElementType
属性。此外Annotation
有很多的子类,包括Target
、Retention
等,其中有一部分是元注解,关于元注解的概念下文将会详细介绍,接下来将要详细介绍的是ElementType
属性和RetentionPolicy
属性。
2、ElementType
ElementType
是枚举类型,用来指定Annotation
的类型,该枚举类型包含以下成员:
成员 | 说明 |
---|---|
TYPE | 类、接口(包括注释类型)或枚举声明 |
FIELD | 字段声明(包括枚举常量) |
METHOD | 方法声明 |
PARAMETER | 参数声明 |
CONSTRUCTOR | 构造方法声明 |
LOCAL_VARIABLE | 局部变量声明 |
ANNOTATION_TYPE | 注释类型声明 |
PACKAGE | 包声明 |
TYPE_PARAMETER | 类型参数声明(自JDK1.8起支持) |
TYPE_USE | 类型的使用(自JDK1.8起支持) |
3、RetentionPolicy
RetentionPolicy
是枚举类型,用来指定Annotation
的策略,该枚举类型包含以下成员:
成员 | 说明 |
---|---|
SOURCE | Annotation信息仅存在于编译器处理期间,编译完成即被删除 |
CLASS | 编译器将Annotation存储于类对应的.class文件中,这是默认行为 |
RUNTIME | 编译器将Annotation存储于class文件中,并且可由JVM读入 |
- 若注解的类型为
SOURCE
,则意味着:注解仅存在于编译器处理期间,编译器处理完之后,该注解就没用了。 例如,@Override
标志就是一个注解。当它修饰一个方法的时候,就意味着该方法覆盖父类的方法;并且在编译期间会进行语法检查!编译器处理完后,@Override
就没有任何作用了。 - 若注解的类型为
CLASS
,则意味着:编译器将注解存储于类对应的.class
文件中,它是注解的默认行为。 - 若注解的类型为
RUNTIME
,则意味着:编译器将注解存储于.class
文件中,并且可由JVM读入。
三、元注解
什么是元注解呢?通俗地讲就是用于描述注解的注解。Java内部已经定义了一些元注解,下面通过表格介绍一些常用的元注解:
注解名称 | 作用 |
---|---|
@Target | 描述注解能够作用的位置,需要传入ElementType 类型的值 |
@Retention | 描述注解被保留的阶段,需要传入RetentionPolicy 类型的值 |
@Documented | 描述注解是否被抽取到API文档中 |
@Inherited | 描述注解是否被子类继承 |
若需获取更多关于注解的信息,请参阅:java.lang.annotation
四、自定义注解
在介绍完注解的实现原理以及元注解的相关概念之后,我们就可以自定义注解了,下面将详细介绍如何自定义注解。
1、注解的格式
定义注解的格式为:
1 | @元注解 |
其中元注解的含义上文已经介绍过了,这里不再赘述。属性实际上就是接口中的抽象方法。下面将举一个简单的例子来说明如何定义一个注解:
1 | package my.study.annotation.anno; |
2、注解的属性定义
关于注解中的属性,有以下几点要求:
(1)属性的返回值类型必须为以下几种类型
- 基本数据类型
- String
- 枚举
- 注解
- 以上类型的数组
(2)定义了属性,在使用时需要给属性赋值
注:如果定义属性值时使用default
关键字给属性默认初始化值,则使用注解时可以不进行赋值。此外,如果只有一个属性需要赋值,且需要赋值的属性名称为value
,则使用注解时可直接传入value
的属性值,value
属性名可省略。
1 | package my.study.annotation.anno; |
(3)属性返回值类型为数组的注解在赋值时传入的值用{}
包裹,如果数组中只有一个值,则{}
省略
1 | // 在传入多个值时用{}包裹 |
五、案例1:实现一个简单的测试框架
1、需求
如果我们需要实现一个简单的测试框架,该框架在运行之后会自行检测所有加上@Check
注解的方法,并判断该方法是否有异常,而且要将检测的结果记录到文件中。下面给出@Check
注解和被检测的Calculator
类的定义:
(1)@Check
注解
1 | package my.study.annotation.anno; |
(2)Calculator
类
1 | package my.study.annotation.pojo; |
2、分析
要想实现该功能,需要利用Java中的反射机制。首先查找Calculator
类中所有加上@Check
注解的方法,之后执行该方法,对可能出现的异常进行捕获,并记录到文件中。
3、实现
完整的功能实现如下:
1 | package my.study.annotation; |
运行程序,bug.txt
中的内容如下:
1 | div方法出现异常 |
六、案例2:利用注解代替配置文件改写框架
1、需求
在之前一篇关于Java反射机制的文章中,我们在文章的最后提到了一个案例,该案例要求写一个“框架”,可以创建任意类的对象并执行其中任意方法。在那篇文章中我们利用反射和配置文件来实现了“框架”,其实可以用注解来代替配置文件,同样可以实现该功能。那么该如何去写呢?
2、分析
可以编写一个注解,该注解包含className
和methodName
两个属性,在功能实现类上加上该注解,并传入属性值。这样就可以利用反射机制获取到功能实现类的注解,进而获取到传入的属性值。后面的步骤就与之前的实现方法相同,这里不再赘述。
3、实现
(1)@Properties
注解
1 | package my.study.annotation.anno; |
(2)功能实现类
1 | package my.study.annotation; |
运行程序,结果与之前的结果相同,这里不再赘述。
七、总结
注解本质上就是用来描述类、方法或者成员变量的代码级说明,在实际运用中常常与反射机制结合在一起使用。使用注解可以摆脱框架中复杂的配置文件,使开发人员可以专注于业务逻辑的实现。因此掌握注解相关的知识是非常重要的。