Java 中的注解(Annotation)

博客闲置了三年,近日在工作四年之后,找工作不太顺,反思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”的一般方法和思路,此处又杜撰出一个名字,大家见谅,理解就好。




猜你喜欢

转载自blog.csdn.net/ding3106/article/details/80664934