Java基础加强:Java注解
上篇我们讲述了框架中核心技术 Java 反射 的使用,介绍了它功能的强大之处。
本篇我们讲述另一个框架使用的重要技术,Java 注解
1. 什么是注解
1.1 基本介绍
注解(Annotation),也叫元数据。一种代码级别的说明。它是 JDK1.5 及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明。
举个例子,注释我们都知道,注释是对程序的一些说明,是为了让程序员看得懂程序而编写的。
而注解也是对程序的说明,不过是给计算机看的,计算机通过注解得到信息,可以把注解理解为标签,给类、方法、成员变量贴上标签,对他们进行一些说明。
在使用 Junit 单元测试时,我们在每一个单元测试的方法上都要加上一个@Test
注解,这个注解就是告诉测试程序这个方法是测试方法,是可以用作单元测试的,可以直接运行:
/** |
1.2 注解的基本结构
接下来我们从源码找寻注解的结构。
首先我们查看 SuppressWarnings 注解内部内容:
|
可以看到,里面的结构为两个注解 + 一个用@interface声明的名称,该名称{}
范围中还有一个类似方法定义的String[] value()
。
1.2.1 注解定义分析
首先我们看@interface
,它其实是一种标识符,标识声明为一个注解,就如同class
标识为声明为类一样。
那么 @interface 内部到底是如何实现注解定义的呢,我们可以通过反编译查看其中的内容,首先我们在任意目录下新建一个 MyAnno.java 文件,然后打开命令行窗口并定位到该目录,执行以下命令:
javac MyAnno.java //编译形成class文件 |
可以看到在执行第二条命令的下面出现以下代码信息:
Compiled from "MyAnno.java" |
看到这里我们就很熟悉了,这不就是声明了一个名字为 MyAnno 的接口然后继承了 java.lang.annotation.Annotation
吗,那么 java.lang.annotation.Annotation
是啥,我们打开API文档,搜索 Annotation ,可以看到这么一段内容:
- public interface Annotation
所有注解类型扩展的公共接口。 注意,手动扩展这个接口不限定注解类型。 还要注意,此接口本身并不定义注解类型。 有关注解类型的更多信息,请参见The Java™ Language Specification的第9.6 节 。
AnnotatedElement
接口讨论了将注解类型从不可重复性转变为可重复时的兼容性问题。
到这基本就清楚了,@interface 用来定义一个注解,而且注解底层本质是一个接口,所有注解默认继承了 java.lang.annotation.Annotation
接口 当然要注意,注解不支持继承,不可以使用 extends 关键字继承自 @interface 。
1.2.2 元注解
我们再观察上面两个注解 @Target 和 @Retention 。
@Target
,@Retention
两个注解其实是由 Java 提供的 元注解 ,所谓元注解就是注解的注解,可能有点懵,我这样解释一下:
比如在类上面使用注解是表述类或描述类的一些信息,而元注解在注解中的使用就是用来表述或定义注解的一些信息。下面我们分别对这两个元注解进行分析。
@Target注解
查看 @Target 注解的内容如下:
public Target {
/**
* Returns an array of the kinds of elements an annotation type
* can be applied to.
* @return an array of the kinds of elements an annotation type
* can be applied to
*/
//返回注释类型的元素种类的数组
ElementType[] value();
}可以看到注解内容中有一个
ElementType[] value()
。ElementType 为枚举类型,其作用是帮助我们在定义 Target 注解时指定传入的值,其中定义的内容如下:
* Joshua Bloch
* 1.5 // 1.5版本之后可用
* 9.6.4.1
* 4.1 The Kinds of Types and Values
*/
public enum ElementType {
/** Class, interface (including annotation type), or enum declaration */
//表明该注解可以用于类、接口、(包括注解类型)或枚举类型之上
TYPE,
/** Field declaration (includes enum constants) */
//表明该注解可用于字段,包括enum常量之上
FIELD,
/** Method declaration */
//表明该注解可用于方法之上
METHOD,
/** Formal parameter declaration */
//表明该注解可用于方法参数之上
PARAMETER,
/** Constructor declaration */
//表明可用于构造函数之上
CONSTRUCTOR,
/** Local variable declaration */
//表明该注解可用于局部变量之上
LOCAL_VARIABLE,
/** Annotation type declaration */
//表明该注解可用于注解之上
ANNOTATION_TYPE,
/** Package declaration */
//表明该注解可以用于包之上
PACKAGE,
/**
* Type parameter declaration
*
* @since 1.8 从java 1.8 开始可用
*/
//表明该注解可用于类型参数
TYPE_PARAMETER,
/**
* Use of a type
*
* @since 1.8 从java 1.8 开始可用
*/
//表明该注解可用于类型使用
TYPE_USE
}解释:
Target 是目标的意思,@Target 就是约束注解可以使用的范围,例如我想约束注解只可作用于方法上,那么我就需要在注解的定义上声明如下:
//注解作用于方法上
public AnnoBasic {
}Target 中的成员变量
ElementType[] value()
是一个数组,也就是意味着可以同时出传入多个类型相同但数值不同的值。例如我想使注解作用于类、方法、成员变量上,就可以这样定义:
//注解作用于类、方法、成员变量上。
public AnnoBasic {
}不过在这里要注意使用大括号
{}
将这些属性括起来表示为数组。还有一点,当注解上没有使用 @Target 注解约束时,表示该注解可以作用于任何元素之上。
@Retention注解
查看@Retention注解的内容:
public Retention {
/**
* Returns the retention policy.
* @return the retention policy
*/
//返回保留的策略
RetentionPolicy value();
}类似 @Target 注解,这里也有一个成员属性值 value(),类型为
RetentionPolicy
。RetentionPolicy也是枚举类型,其中封装的值帮助我们定义@Retention注解指定传入的值,其中内容如下:public enum RetentionPolicy {
/**
* Annotations are to be discarded by the compiler.
*/
//表明注解被编译器丢弃(该类型的注解信息只会保留在源码里)
//源码经过编译后,注解信息就会被丢弃,不会保留在class文件中
SOURCE,
/**
* Annotations are to be recorded in the class file by the compiler
* but need not be retained by the VM at run time. This is the default
* behavior.
*/
//表明注解会被记录在class文件中,但VM(虚拟机)在运行时不会被保留该注解,该内容不会被加载到虚拟机中。
//在不手动指定注解的生命周期时,默认注解声明周期都是该值
CLASS,
/**
* Annotations are to be recorded in the class file by the compiler and
* retained by the VM at run time, so they may be read reflectively.
*
* @see java.lang.reflect.AnnotatedElement
*/
4//表明该注解会被记录在class文件中而且在VM(虚拟机)运行时会被保留
//虚拟机会加载class文件中的该注解的内容,可以使用反射机制读取该注解的内容
RUNTIME
}解释:
具体每个属性的信息可以查看上面的代码注释,说明的很清楚,我再说几点。
第一,类似@Override、@SuppressWarning这些对代码编写规范的提示类注解,他们的作用周期都是 SOURCE 也就是源码阶段。
第二,在目前的主流框架中,将注解定义为 RUNTIME 运行时时用的比较多的,如SpringMvc中的@Controller、@Autowired、@RequestMapping等。定义在运行时,我们可以通过反射获取注解的内容,可以简化代码。
@Inherited注解
@Inherited 负责指定注解可以被继承,但不是真正意义上的继承,注解本质上是不可以继承的,我们无法使用
extends
标识符继承自 @interface 。这里的继承指得是可以让子类通过反射使用Class对象中的getAnnotations() 获取父类被@Inherited修饰的注解,具体请看以下示例:
/**
* @author qianfanguojin
* /@Inherited表明注解可继承的示例
*
*/
Anno1{
String value() default "Anno1被使用";
//定义@Inherited的注解
}
Anno2{
String value() default "Anno2被使用";
//没有定义@Inherited的注解
}
class A{
}
class B extends A{
public static void main(String[] args) throws ClassNotFoundException {
//获得子类B的Class对象
Class cls = Class.forName("cn.annotation.B");
44//获得子类B上面定义的所有注解(注意该注解是要定义在类的声明上面的)
Annotation[] ano = cls.getAnnotations();
for(Annotation a : ano) {
//获得的注解类型如果来自Anno1注解,输出该注解中value的值
if(a instanceof Anno1) {
System.out.println(((Anno1)a).value());
}
//获得的注解类型如果来自Anno2注解,输出该注解中value的值
if (a instanceof Anno2) {
System.out.println(((Anno2)a).value());
}
}
}
}输出结果:
Anno1被使用
@Documented
说明该注解将被包含在javadoc中。和注释一样,注解虽然会在源码中显示,但是 javadoc 命令生成的文档默认会隐藏注解内容,如果我们想让某个注解在 javadoc 中可显示,在注解中定义即可,如:
/**
* 定义注解可以在java文档中出现
*/
@Documented
@interface DO{
}
1.2.3 注解元素(属性)
p1. 基本定义和使用
在上面 @SuppressWarning 注解的定义内容中,有一个String[] value()
。
这其实是注解的属性,或者说是元素。在前面的分析中,我们知道,注解的本质是一个接口,在接口中,我们可以定义抽象方法,在注解中我们同样可以,只不过在注解中,抽象方法被用来表示注解的元素。
于是在注解中注解元素的定义以 无形参的方法 形式声明,方法名定义了该元素的名字,返回值定义了该元素的类型。
在大多数自定义注解中,一般都会声明一些元素以标识某些值,方便程序使用,比如我们上面使用的 Target 注解,它在注解定义中定义了一个成员属性,那么我们就可以在声明注解的时候传入相关值来实现不同的效果:
//注解应用于方法上 |
在注解定义中,支持如下的数据类型:
- 所有的基本类型(int,float,boolean,byte,double,char,long,short)
- String 字符串
- Class 类对象
- enum 枚举
- Annotation 注解
- 上述类型的数组
除以上的类型之外,在注解中不可使用其他类型或是说基础类型的包装类型如 Integer
,不然编译器会报错。
还有一点就是和定义成员变量一样,注解元素也支持定义默认值,在某个元素定义了默认值后,我们在使用注解时可以不指定该元素的值。定义默认值方法是在定义的注解元素名后用 defult
指定。
使用示例如下:
package cn.annotation; |
注意两点:
在定义默认值时要注意,编译器对注解属性的默认值使用规定非常严格。
首先是任何一个元素的默认值都不能设定为
null
,在使用注解时也不能将元素的值指定为null ;其次是任一元素不能有不确定的值,要么在注解的定义中声明元素的默认值,不然就必须要在代码中使用注解时指定该元素的值。对于有些情况我们想让其表示为空时,只能用空自字符串或者是0等其他参数代替。使用数组作为注解元素时,在使用该注解需要指定多个值时,需要用
{}
将数值包含起来,如:
p2. 使用快捷方式指定注解元素的值
在使用注解时,我们要指定注解元素的值,一般来说都是使用 key=value
的方式,比如:
但有的时候我们并不需要指定所有注解元素的值,或者说只有一个注解元素的值需要指定,在注解中提供了一个元素名为 value()
的元素,只要在注解的定义中定义了名为 value
的元素,我们在使用注解指定该值时就可以直接在括号中填入该值,最明显的一个案例就是 @Target 注解的内容:
|
定义了元素名为 value() 后,我们在使用时就可以直接填入该值的内容,如:
2. 注解的基本使用
在 Java 中,注解的语法格式为: @+注解名
,如@Override
。
一般来说,注解的使用主要分为以下三类:
- 编写文档。在代码的文档注释里使用 Java 提供的注解,然后使用 javadoc 命令生成doc文档。
- 编译检查。通过代码里标识的注解能够让编译器实现编译检查,如 【Override】命令检查是否为继承重写的方法。
- 代码分析。通过代码里标识的注解对代码进行分析,处理。
在现代的主流框架中,如Spring ,大量使用了注解来简化代码,那么现在我们就来分析注解具体的一些我们应该学习的地方。
2.1 使用注解编写文档
使用 Java 时,我们经常会遇到自己不认识的类或者是某个方法我们不知道具体是做什么用的,此时我们都知道可以翻阅 java 的 api 文档查看,如 java8 的文档:
那么这些文档怎么生成的呢?
其实这些都是使用 javadoc 命令通过解析代码中的文档注释以及相关注解自动生成的网页。
现在我们用一个简单的例子测试以下:
/** |
然后在 AnnoDocDemo.java 文件所在目录打开命令行窗口,执行:
javadoc AnnoDocDemo.java |
此时在目录下会生成许多网页相关的文件:
然后我们打开 index.html 可以看到:
生成了跟我们的 api 文档一样的样子,而且刚才我们定义的注解都被解析成文档的格式
如@since
表示从何版本开始,方法中的@return
表示对返回值的解释。
当然在文档注释中可以使用的注解不止这些,通过使用这些注解可以帮助 javadoc命令 在生成文档的时候根据注解内容来生成不同的文档内容。也就是说,注解能让 java 程序通过注解得到程序想要的信息。
2.2 JDK 内置的一些注解
在 JDK 中预置的三个常用注解为:
@Override
:检测该被注解标注的方法是否是继承自父类(接口)的。源码为:
public Override {
}@Deprecated
:被该注解标注的内容,表示已经过时。源码为:
public Deprecated {
}@SuppressWarning
:有选择的关闭编译器对类、方法、成员变量、变量初始警告。源码为:
public SuppressWarnings {
String[] value();
}内部的 String 数组接收的值如下:
deprecation:使用了不赞成使用的类或方法时的警告;
unchecked:执行了未检查的转换时的警告,例如当使用集合时没有用泛型 (Generics) 来指定集合保存的类型;
fallthrough:当 Switch 程序块直接通往下一种情况而没有 Break 时的警告;
path:在类路径、源文件路径等中有不存在的路径时的警告;
serial:当在可序列化的类上缺少 serialVersionUID 定义时的警告;
finally:任何 finally 子句不能正常完成时的警告;
all:关于以上所有情况的警告。
使用介绍如下:
2.2.1 @Override
首先是@Override
注解,添加如下代码
/** |
可以看到,编辑器报错,并提示 Method dose not override method from its superclass
也就是该方法不是重写父类的,声明@Override注解
会报错。
2.2.2 @Deprecated
其次是@Deprecated
注解,在刚才的文件中添加代码:
/** |
这时我们如果调用 add()
方法时,会出现:
提示我们方法过时,在 Java 的发展中,强大的类库为我们提供了许多的方便,不过有些方法或类久了之后在程序中使用已经不再推荐,如 Java 中的 Data 类z中的许多方法:
A link to jump towards target header
2.2.3 @SuppressWarning
最后是@SuppressWarning
注解,我们在编辑器编写代码时候,如:
这里每一小点点都是编译器帮我们检查出来的一些可能的问题,编辑器会以一些方式来提醒用户,但是有得时候警告可能很多,而且我们自己其实知道代码并没有什么问题,我们可以使用@SuppressWarning
注解来压制警告。
@SuppressWarning
压制警告的范围和类型由自己决定,比如我想压制整个类的所有警告类型,我在类名上声明注解为:
在类上声明,该注解会作用于整个类的区域,后面的字符串指定你要压制的类型,一般我们使用al
即全部类型的警告,其余的属性请翻阅上面的 JDK 内置的一些注解 (按住Ctrl + 鼠标左键跳转)。
3. 注解进阶使用
3.1 获取使用注解对象
在使用自定注解之前,我们先来了解一下如何获取注解中的内容。
根据前面的知识,我们知道注解声明的位置是在类、方法、成员变量….等等之上,如果接触过反射的话,我们知道在程序的运行过程中我们可以通过反射机制获取类中的构造方法、成员方法、成员变量等,而注解也可以看作是类、方法、成员变量中的一部分。
为了在运行时准确的获取到注解的相关信息,Java 在java.lang.reflect 反射包下提供了AnnotatedElement 接口,反射我们使用的基本类如Method类、Constructor类、Field类、Class类和Package类都实现了该接口。
该接口中的方法及功能如下:
方法返回值 | 方法名 | 方法功能 |
---|---|---|
<T extends Annotation> T |
getAnnotation(Class<T> annotationClass) |
返回该元素的指定注解类型的注解对象(包括继承自父类的),如找不到该注解类型,则返回 null。 |
Annotation[] |
getAnnotations() |
返回该元素上所有存在的注解,包括继承至父类的。 |
default <T extends Annotation> T |
getDeclaredAnnotation( Class <T> annotationClass ) |
返回该元素的指定注解类型的注解对象,如找不到该注解类型,则返回 null,忽略继承自父类的注解。 |
Annotation[] |
getDeclaredAnnotations() |
返回该元素上所有存在的注解,忽略继承至父类的注解。 |
default boolean |
isAnnotationPresent( Class <? extends Annotation> annotationClass ) |
如果此元素上存在指定类型的注解(包括继承自父类的),则返回true,否则返回false。 该方法主要用于方便访问标记注解。 |
一个简单的使用案例如下:
package cn.annotation; |
输出的结果为:
----------getAnnotation 获取该元素指定注解类型的注解对象,包含继承的---------- |
要注意一点,虽然getAnnotations 和getAnnotation 都能获取到从父类继承的注解,但是父类的注解必须被声明 @inherited 注解,子类才能获取到该父类的注解。
3.2 自定义注解实现对方法的测试
了解完注解的基础知识以及如何使用反射获取注解之后,我们通过一个例子自定义注解解决我们的问题。
假设小明写了一个类 Calculator ,他自信满满的对我说:
小明:“像我这么优秀的人,我写的代码肯定是没有问题的”
我:“自信是好的,但是还是不要轻心,还是测试比较一下比较好的”
小明:“那你想怎么测试,一个个调用很麻烦的,还要一个个分析”
我:“不用,我只要使用注解@QTest就好了,你看我表现吧”
小明编写的类 Calculator :
/** |
测试使用的注解 AnnoCheck:
import java.lang.annotation.ElementType; |
最后,我使用自己的测试类 CalculatorCheck 就可以测试 Calculator 类中的方法了:
/** |
输出的结果为:
1 + 0 =1 |
可以看到,提示div 方法出现了异常,异常名为 ArithmeticException ,异常原因为除 0 操作。
小明的代码是有问题的,而我也通过注解完成了我的目的。
利用注解 + 反射的结合能帮我们解决平时看起来很复杂的问题,在目前的应用中,注解在各种框架中大放光彩,比如:
JUnit 测试框架
Android 的 ButterKnife
Spring
…..
4. jdk 8 对注解功能的增强
4.1 新增元注解@Repeatable
@Repeatable 注解在Java 8 中加入,它表示的注解可以在同一个位置重复使用。比如:
|
在 java8 之前,如果我们要实现一个注解表示多个相同类型但不同的值,我们必须要在注解内部声明数组,比如@Target注解:
|
我们在使用时在注解上只能声明一次 Target注解,然后指定多个值形成数组,来表示注解可以在多个地方使用:
而Java8又提供了另一种解决方式:使用@Repeatable 注解表明该注解可以在同一元素上重复使用。假设我想使Role 注解可以重复声明在类上,实现这个功能需要两步:
第一步在 Role 注解上加@Repeatable注解,并指定多个重复的Role注解存放的容器,该容器也是一个注解类型:
/** |
第二步创建 Role注解的容器Roles注解,在Roles注解中声明一个 Role [] 注解数组存放多个Role注解:
/** |
然后我们就可以在同一元素上使用Role注解了。
事实上这只是一种语法糖而已,我举个例子,假设我们在Person类上声明了两个相同Role注解:
|
这两个 Role 注解会自动合并到 Roles 注解中,相当于这样定义一个Roles注解:
|
而事实是这样定义和上面定义的效果是一样的,上面的定义看起来可能就更直观一点。
为了处理这种重复注解,java8也在 AnnotatedElement 接口中新增了两个方法,用法如下:
方法返回值 | 方法名 | 方法功能 |
---|---|---|
default <T extends Annotation>T[] |
getAnnotationsByType( Class<T> annotationClass) |
返回该元素上指定注解类型,重复使用的注解,包括继承自父类的。 |
default <T extends Annotation>T[] |
getDeclaredAnnotationsByType(Class<T> annotationClass) |
返回该元素上指定注解类型,重复使用的注解,不包括继承自父类的。 |
注意:
对于 getAnnotationsByType 方法,该方法会最先调用 getDeclaredAnnotationsByType 方法寻找当前元素(类,成员变量,成员方法)上是否有指定注解类型而且被重复声明的注解,如有则返回当前元素上定义的指定注解类型的所有注解。
如找不到,并且注解是声明在一个类上,而该注解又是可继承的(使用了@Inherited),则就去父类寻找是否有指定注解类型重复声明的注解。
两者只能取一,意思是如果你开始获取到子类有指定注解类型的重复注解,那么就不会再去寻找父类是否有该指定类型的重复注解,子类找不到再去寻找父类。
简单的使用示例如下:
//声明两个重复注解 |
输出结果:
.annotation.Role(value=USER) |
根据上面的说明,子类父类只能获取到其中之一的重复注解,我要获取父类的则必须把子类同名的重复注解注释:
//@Role("USER") |
此时我们再运行,结果为:
.annotation.Role(value=MAN) |
成功获取到父类的重复注解。
好了,本篇文章结束,谢谢大家阅读。
5. 参考链接
- 深入理解Java注解类型(@Annotation)
- 秒懂,Java 注解 (Annotation)你可以这样学