博客闲置了三年,近日在工作四年之后,找工作不太顺,反思Android framework从业以来自己的所作所为,技术上其实自己花了大量的精力学习和拓展新东西,但是一直没有“输出”,导致“学未所用”,遂决定重拾荒废了3年的博客,我想只要知道开始,就还不晚。
挑个java中简单点的注解作为出山之作吧。内容主要参考《Java疯狂讲义》对应章节,在充分理解基础上,对该技术点进行总结归纳。
闲话不多说,正式开始。
Annotation,中文翻译大都译为“注解”,是一种java代码中的特殊标记,形式上用于修饰程序元素,这里的程序元素包括类、包、成员变量、成员函数、构造器、局部变量甚至一些代码关键字、参数等等;注解的增删不影响java代码的正常运行,但是,通过注解的使用,可以赋予java代码额外的一些功能或者说特性,当然,这些额外的功能和特性,需要程序猿提供额外的工具(即Annotation Processing Tool, APT)进行解析。不同的注解的有效期是不一样的,取决于程序猿对应去设置该Annotation对应的属性。虽然有效期的属性可以设置成3个值,但是我接触到的情况,有效期也就分两种,一种是只在编译时生效,另一种是运行时有效。
0. Annotation的定义和使用形式
0.1 一个Annotation类型的定义
0.1.1 不带成员变量的定义
定义一个Annotation类型和定义一个接口(interface)非常像,只需要在interface关键字之前加上@符号,即“@interface”
参见以下:
//定义一个简单的Annotation类型
public @interface Test
{
}
0.1.2 带成员变量的定义
Annotation可以带成员变量,其成员变量在Annotation的定义中是以“无形参的方法”形式来声明的,其方法名和返回值分别对应定义了该成员变量的名字和类型
参见以下:
public @interface MyTag
{
//定义带两个成员变量的Annotation
//其中的成员变量以“无形参的方法”形式来定义
String name();
int age();
}
为成员变量指定默认值
如下:
public @interface MyTag
{
//定义带两个成员变量的Annotation
//使用default为两个成员变量指定初始值
String name() default "Tom";
int age() default 13;
}
0.2 Annotation类型的使用形式
默认情况下,Annotation可以用于修饰任何程序元素。
使用Annotation的语法类似于public、final这样的修饰符,通常放在所有修饰符之前。
由于使用Annotation时可以还需要为成员变量指定值,所以Annotation长度可能比较长,通常把Annotation另放一行。
如下:
public Class MayClass{
//使用@Test Annotation修饰方法
@Test
public void info(){
}
}
当Annotation中定义了成员变量,在使用时就应该为该Annotation的成员变量指定值。
如下:
public class Test
{
@MyTag(name="xx", age=6)
public void info()
{
}
}
当Annotation的定义中指定了成员变量的默认值,则使用时可以不为这些成员变量指定值,默认直接使用默认值
1. JDK中五种基本的Annotation
1.1 @Override 限定重写父类方法
注意:该Annotation只能修饰方法,不能修饰其他程序元素
使用方式如下:
//父类定义
public Class Fruit{
public void info()
{
}
}
//子类定义
public Class Apple extends Fruit{
@Override
public void info()
{
}
}
参见以上使用方式。
Override的作用是:告诉编译器检查这个方法,保证父类要包含一个同名同参的方法,否则编译报错
1.2 @Deprecated 标记已过时的程序元素
作用:当程序中使用了被 Deprecated 标记的程序元素时,编译器将会给出警告
如下:
class Apple{
//标记info()方法已过时
@Deprecated
public void info(){}
}
public class DeprecatedTest
{
public void main(){
//使用被Deprecated修饰的方法时编译器会报警
new Apple().info();
}
}
1.3 @SuppressWarnings 抑制编译器警告
作用:对于被该Annotation修饰的程序元素(以及该程序元素中所有的子元素),在编译时会取消显示指定的编译器警告
示例如下:
//关闭整个类中的编译器警告
@SuppressWarnings
public class SuppressWarningsTest
{
public static void main(String[] args)
{
List<String> myList = new ArrayList<>();
}
}
1.4 @SafeVarargs 与“堆污染”警告有关系
设计到堆污染的概念,后续再拓展
1.5 @FunctionalInterface Java8中的函数式接口
Java 8中规定:如果接口只有一个抽象方法(可以包含多个默认方法或多个static方法),该接口就是函数式接口
作用:FunctionalInterface 用来指定某个接口必须是函数式接口
示例如下:
@FunctionalInterface
public interface FunInterface
{
static void foo()
{
System.out.println("静态方法");
}
default void bar()
{
System.out.printl("bar默认方法");
}
void test();//
}
注意:@FunctionalInterface只能修饰接口,不能修饰其他程序元素
2. 元Annotation(Meta Annotation)的概念和介绍
2.1 概念
元Annotation用于修饰其他Annotation,是注解的注解
2.2 五种常见的元注解
2.2.1 @Retention
只能用于修饰Annotation的定义
作用:用于指定被修饰的Annotation可以保留多长时间
JDK 中 Retention 的定义如下:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
/**
* Returns the retention policy.
* @return the retention policy
*/
RetentionPolicy value();
}
其中 Retention 包含一个 RetentionPolicy 类型的 value 成员变量,使用时必须为该 value 成员变量指定值
JDK 中 RetentionPolicy 的定义如下:
public enum RetentionPolicy {
/**
* Annotations are to be discarded by the compiler.
*/
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,
/**
* 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
*/
RUNTIME
}
- RetentionPolicy.CLASS: 编译器将把 Annotation 记录在 class 文件中,在程序运行时不可获取。这是默认值
- RetentionPolicy.RUNTIME: 编译器将把 Annotation 记录在 class 文件中,在程序运行时 JVM 可以获取 Annotation 信息,程序可通过反射获取该 Annotation 信息
- RetentionPolicy.SOURCE: Annotation 信息只保留在源代码中,编译器直接丢弃该 Annotation。一般,在编译阶段通过专门的 Annotation 处理器处理被修饰的 Annotation,但不会在 class 文件中有体现,一般用于生成 XML 文件,后面会有详解
使用实例:
//定义下面的 Testable Annotation 信息有效时间保留到程序运行时
@Retention(value= RetentionPolicy.RUNTIME)
public @interface Testable{}
关于指定成员变量的一些特殊提示:
当 Annotation 的成员变量名为 value 时,程序中可以直接在Annotation后的括号里指定该成员变量的值,无须使用“value=变量值”的形式
例如以上示例可以修改如下:
//无须使用 “value = 变量值” 的形式
@Retention(RetentionPolicy.RUNTIME)
public @interface Testable{}
2.2.2 @Target
只能修饰一个Annotation的定义(只能在一个Annotation的定义中使用)
作用:用于限定被修饰的Annotation 能用于修饰哪些程序元素
JDK中@Target的定义如下:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface 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();
}
如以上定义:@Target中有个类型必须为ElementType的成员变量
ElementType在JDK中的定义如下:
public enum ElementType {
/** Class, interface (including annotation type), or enum declaration */
TYPE,
/** Field declaration (includes enum constants) */
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
*/
TYPE_PARAMETER,
/**
* Use of a type
*
* @since 1.8
*/
TYPE_USE
}
- Element.ANNOTATION_TYPE: 限定被修饰的Annotation只能修饰 Annotation
- Element.CONSTRUCTOR: 限定被修饰的 Annotation 只能修饰构造器
- Element.FIELD: 限定被修饰的 Annotation 只能修饰成员变量
- Element.LOCAL_VARIABLE: 限定被修饰的 Annotation 只能修饰方法定义
- Element.METHOD: 限定被修饰的 Annotation 只能修饰包定义
- Element.PACKAGE: 限定被修饰的 Annotation 只能修饰参数
- Element.PARAMETER: 指定被修饰的 Annotation 可以修饰参数
- Element.TYPE: 指定被修饰的 Annotation 可以修饰类、接口(包括注解类型)或者枚举定义
JDK 1.8新增了以下两个新的ElementType枚举值:
- Element.TYPE_PARAMETER:
- Element.TYPE_USE: 被 @Target(Element.TYPE_USE) 修饰的注解被称为 Type Annotion (类型注解)
类型注解可以在任何用到类型的地方使用。
2.2.3 @Documented
作用:被 @Documented 修饰的 Annotation,将被 javadoc 工具提取成文档。通俗点来说,被 @Documented修饰的Annotation,将出现在 javadoc 工具生成的文档中。
java 中通过 javadoc 工具,可以提取源代码文件中的相关注释信息,并生成“文档”,供用户查询
意思理解即可,此处省略实例代码。
2.2.4 @Inherited
作用:@Inherited 可以使被修饰的 Annotation 具有继承性,即:当某个父类被一个 Annotation 修饰(这个 Annotation 被@Inherited 修饰),这个父类的子类将自动被这个 Annotation 修饰。
实例如下:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Inheritable
{
}
上面的 Annotation Inheritable 被 @Inherited 修饰,再使用 @Inheritable 修饰一个父类,如下:
@Inheritable
class Base{
}
//子类如下
public class InheritableTest extends Base
{
public static void main(String[] args)
{
//打印InheritableTest类是否有@Inheritable修饰
System.out.println(InheritableTest.class.isAnnotationPresent(Inheritable.class));
}
}
以上代码运行后输出:true。原因见上述 @Inherited 的作用
2.2.5 @Repeatable
Java 8 中新增的重复注解
2.2.5.1 Java 8 之前重复注解的使用
@Results({@Result(name = "failure", location = "failed.jsp"),
@Result(name = "success", location = "succ.jsp")})
public class Test
{
}
Java 8 之前不支持在同一个程序元素前使用多个“类型相同的 Annotation”,需要使用 Annotation“容器”。上面的@Results 就是一种“注解容器”,该容器只包含一个名字为 value、类型是 Result[] 的成员变量。
2.2.5.2 Java 8 之后重复注解的使用
Java 8 之后可以对 2.2.5.1 中的写法进行简化,允许使用多个相同类型的 Annotation 来修饰同一个程序元素,示例如下:
@Result(name = "failure", location = "failed.jsp")
@Result(name = "success", location = "succ.jsp")
public class Test
{
}
2.2.5.3 Java 8 之后“新重复注解”的定义
要使用2.2.5.2中的简化形式,必须要预先做“重复注解的定义”,定义过程中就用到了本小节的重点:@Repeatable
此处的定义步骤遇到了一个“鸡生蛋,蛋生鸡”的难题,个人觉得两步可以互换:
第一步:定义可重复的注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Repeatable(FkTags.class)
public @interface FkTag
{
String name() default "张三";
int age();
}
上述@Repeatable在使用时,为其 value 成员变量指定了“容器”注解,这个“容器”注解的定义见第二步。
第二步:定义第一步中可重复注解的“容器”注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FkTags
{
//定义value成员变量,该成员变量可接受多个@FkTag注解
FkTag[] value();
}
Java 8 之后,2.2.5.1 和 2.2.5.2 中的两种写法,都是支持的。
3. 运行时Annotation的提取
Annotation 不会自动生效,需要开发者提供相应的工具来提取并处理这些 Annotation 信息。这些工具一般是专门写的“代码处理逻辑”。
3.1 Annotation 提取的理论依据
Java使用 “Annotation 接口”来代表程序元素前面的“注解”,这个接口是“所有注解”的父接口。
Java 5之后,java.lang.reflect 包下新增了一个 AnnotatedElement 接口,该接口代表程序中可以被Annotation修饰的程序元素。这个接口主要由以下几个实现类:
- Class: 类定义
- Constructor: 构造器定义
- Field: 类的成员变量定义
- Method: 类的方法定义
- Package: 类的包定义
也就是说,上述几个类,由于实现了 AnnotatedElement 接口,所以它们可以被 Annotation 修饰。
因此,在获取上述实现类的对象后,可以通过各自实现的 AnnotatedElement 接口中的方法得当 Annotation 的相关信息。
但是有个问题,一个 Annotation 只有被 @Retention(RetentionPolicy.RUNTIME) 修饰,该 Annotation 才在“运行时”可见,JVM 才会在装载 *.class 文件时读取到保存在 class 文件中的 Annotation。所以,本节讨论的“提取”只针对被 @Retention(RetentionPolicy.RUNTIME) 修饰的 Annotation。
被 Retention(RetentionPolicy.SOURCE) 修饰的 Annotation 的提取将在第 4 节介绍
3.2 提取 Annotation 使用的方法
提取 Annotation 所需要使用的方法都被声明在了 AnnotatedElement 接口中,具体如下:
- <A extends Annotation> A getAnnotation(Class<A> annotationClass): 返回该程序元素上存在的、指定类型的注解,如果改类型的注解不存在,则返回Null
- <A extends Annotation> A getDeclaredAnnotation(Class<A> annotationClass): 该方法是 Java 8新增的,该方法尝试获取直接修饰该程序元素、指定类型的Annotation,所谓“直接修饰”,不包含继承来的注解。
- Annotation[] getAnnotations(): 返回该程序元素上存在的所有注解
- Annotation[] getDeclaredAnnotations(): 返回“直接修饰”该程序元素的所有注解
- boolean isAnnotationPresent(Class<? extends Annotation> annotationClass): 判断该程序元素是否存在指定类型的注解
- <A extends Annotation> A[] getAnnotationsByType(Class<A> annotationClass): 该方法时Java 8新增的,与之前的getAnnotation 方法类似,区别在于:由于Java 8新增了重复注解的功能,这个方法可以获取多个重复注解。
- <A extends Annotation> A[] getDeclaredAnnotationsByType(Class<A> annotationClass): 该方法用于获取“直接修饰”该程序元素、指定类型的“重复注解”
实际操作在代码提取 Annotation 过程中,首先,获取程序元素的对象(类对象,Field 对象,Method 对象等),然后调用这些实例的上述方法,获取相应的 Annotation 对象(即相应的Annotation class 对象);然后,可以进一步访问 Annotation 中的成员变量;最后,根据这些成员变量做进一步复杂的逻辑处理。
例如,通过注解,实现:向按钮控件注册监听事件。具体代码此处暂不列出,推荐大家问度娘。
4. 编译时处理的 Annotation
以上介绍了有效期至运行时的Annotation的提取,对于有效期只在编译期的Annotation(被 Retention(RetentionPolicy.SOURCE) 修饰),需要与第三节不同的方法实现 Annotation 信息的提取与处理。
APT(Annotation Processing Tool)是一种注解处理工具,它对源代码文件进行检测,并找出源文件中所包含的Annotation信息,然后针对 Annotation 信息进行额外处理。
4.1 编译时如何指定 Annotation 处理器
对于有效期只在编译时的 Annotation,可以在编译时指定 Annotation 处理器,具体来说,Java 的编译工具 javac.exe,该编译工具有一个“-processor”选项,该选项用于指定“编译时 Annotation 处理器”,这个“编译时 Annotation 处理器”是我杜撰出来的名字,代表只处理有效期在编译时的 Annotation 的处理器。
具体指定形式参考如下:
javac -processor CompileAnnotationProcessor Test.java
Test.java 是包含了待处理 Annotation 信息的源码文件,CompileAnnotationProcessor是“编译时Annotation处理器”的类名。
4.2“编译时Annotation处理器”的一般实现方法:
实现一个“编译时Annotation处理器”,其实就是写一个“编译时 Annotation 处理类”。
每个Annotation处理器(定义的 CompileAnnotationProcessor 类)都需要实现 javax.annotation.processing包下的Processor 接口,但是,实现该接口必须实现该接口中的所有方法,比较麻烦。通常,会采用继承 AbstractProcessor 的方式来实现 Annotation 处理器,这种方式只需要实现 process() 方法,这个 process() 方法中会提供一个 RoundEnvironment 变量,这个变量提供了一些类似于第3节中介绍的方法,使用这些方法可以得到 Annotation 信息。
具体事例,此处也就不再列出,大家感兴趣可以问度娘。本小节只介绍处理“编译时Annotation”的一般方法和思路,此处又杜撰出一个名字,大家见谅,理解就好。