Java注解(annotation)机制

前言

jdk1.5引入了注解机制Annotation),用于对java里面的元素(如:Class、Method、Field等等)进行标记。同时,java的反射类库也加入了对Annotation的支持,因此我们可以利用反射来对特殊的Annotation进行特殊的处理,增强代码的语义。

本文主要是对Annotation的语法Annotation的用法进行分析阐述。然后对一些java自带的、常用的Annotation进行说明,加深读者的理解。

整体结构

借用网上的一张图,来说明整体结构。

Annotation整体结构图

通过这张图我们看到下面的信息

  1. 一个Annotation是一个接口
  2. 一个Annotation和一个RetentionPolicy关联
  3. 一个Annotation和多个ElementType关联
  4. Annotation可以有很多实现,包括java自带的@Override@Deprecated@Inherited等等,用户也可以自己定义

为了更好地理解Annotation,我们将Annotation整体结构拆分为左右两个部分来讲解。

Annotation的组成

先来看看整体结构的左边部分

整体结构左边部分

Annotation关联了3个重要的类,分别为AnnotationElementTypeRetentionPolicy,这3个类所在的包为java.lang.annotation,下面我们来看看java里面的定义。

1. Annotation接口

public interface Annotation {
    boolean equals(Object obj);
    int hashCode();
    String toString();
    Class<? extends Annotation> annotationType();
}

2. ElementType枚举

public enum ElementType {
    /** Class, interface (including annotation type), or enum declaration */
    TYPE, // 类、接口(包括annotation类型)或者枚举声明

    /** 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, // annotation类型声明

    /** Package declaration */
    PACKAGE, // 包声明

    /**
     * Type parameter declaration
     *
     * @since 1.8
     */
    TYPE_PARAMETER, // 类型参数声明(泛型的类型参数)

    /**
     * Use of a type
     *
     * @since 1.8
     */
    TYPE_USE
}

3. RetentionPolicy枚举

public enum RetentionPolicy {
    /**
     * Annotations are to be discarded by the compiler.
     */
    SOURCE, // 仅用于编译期间,编译完成之后,该annotation就无用了

    /**
     * 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, // 编译器将该annotation编译进class里面,但vm运行期间不会保留,默认行为

    /**
     * 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 // 编译器将该annotation编译近class,同时vm运行期间也可以对此annotation使用反射读取
}

接下来我们对这3个类做一下总结

  1. Annotation是一个接口,一个Annotation包含一个RetentionPolicy多个ElementType
  2. ElementType是类型属性,一种类型可以简单地看成一种用途,每个Annotation可以有多种用途
  3. RetentionPolicy是策略属性,共有3种策略,每个Annotation可选择一种策略,默认为CLASS策略
    • SOURCE,仅用于编译期间,编译完成之后,该Annotation就无用了。@Override就属于这种策略,仅仅在编译期检查子类是否可覆盖父类
    • CLASS,编译期将该Annotation编译进class里面,但vm运行期间不会保留,默认行为
    • RUNTIME,编译期将该Annotation编译进class,同时vm运行期间也可以对此Annotation使用反射读取

通用定义

我们来看看Annotation的通用定义,先来写一个自定义Annotation

@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
    String value();
}
  • @interface -- 该修饰符是在关键字interface前加了一个@表示这是一个Annotation
  • @Documented -- 表示该Annotation可以在javadoc中出现,默认不会在javadoc出现
  • @Target(ElementType.TYPE) -- 表示该Annotation可以出现在类型声明上面,比如类、接口、枚举等等
  • @Retention(RetentionPolicy.RUNTIME) -- 表示该Annotation可以被编译进class,同时在运行期vm也可以通过反射读取到它
  • Annotation可以声明成员变量,需要注意以下几点
    1. 声明的方式为无参方法,且没有实现体,如:{type} methodName();
    2. 方法的返回类型为成员变量类型
    3. 方法名为成员变量名字
    4. 成员变量类型只能是基本类型String自定义枚举,其他的Interface、Class等都不能当成成员变量
    5. 成员变量可以使用default来设置默认值,使用时可以不传入值,如:{type} methodName() default {defaultValue};

常用Annotation

我们再来看整体结构的右边部分,我们看到很多我们经常接触到的Annotation。我们会逐一来分析一下。

整体结构的右边部分

【元注解】:网上有元注解一说,其实注解上面的注解叫做元注解。当我们通过@Target(ElementType.ANNOTATION_TYPE)来修饰一个Annotation的时候,就表示该Annotation是一个元注解。

在jdk里面,有一些Annotation是经常用到的,为了加深我们对Annotation的理解,我们需要对这些Annotation进行分析。

  • @Documented,所标注内容,可以出现在javadoc中。
  • @Inherited,只能被用来标注Annotation类型,它所标注的Annotation具有继承性。
  • @Retention,只能被用来标注Annotation类型,而且它被用来指定AnnotationRetentionPolicy属性。
  • @Target,只能被用来标注Annotation类型,而且它被用来指定AnnotationElementType属性。
  • @Deprecated,所标注内容,不再被建议使用。
  • @Override,只能标注方法,表示该方法覆盖父类中的方法。
  • @SuppressWarnings,所标注内容产生的警告,编译器会对这些警告保持静默。

1. @Inherited

我们写一个例子就能很容易看出@Inherited的作用了。

public class InheritedTest {

    @Documented
    @Inherited
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME) @interface MyAnnotation {
    }

    @MyAnnotation class Father {
    }

    class Child extends Father {
    }

    public static void main(String[] args) {
        MyAnnotation[] annotations = Child.class.getAnnotationsByType(MyAnnotation.class);
        System.out.println("annotations length: " + annotations.length);
        for (MyAnnotation annotation : annotations) {
            System.out.println(annotation);
        }
    }
}

Annotation@Inherited修饰时,运行结果如下:

使用@Inherited的效果

Annotation不用@Inherited修饰时,运行结果如下:

不使用@Inherited的效果

2. @Retention

  1. RetentionPolicy.SOURCE类型的class结构和运行效果

class结构图

运行效果图

  1. RetentionPolicy.CLASS类型的class结构和运行效果

    class结构图

运行效果图

  1. RetentionPolicy.RUNTIME类型的class结构和运行效果

    class结构图

运行效果图

3. @Target

ElementType.TYPE修饰的Annotation不能作用于字段和方法,在IntellijIdea里面直接就有错误提示,同时编译的时候也会出错。

IDEA警告

编译错误

4. @Deprecated

使用@Deprecated修饰的类或方法,编译不会出错,运行也不会出错,但是会给出警告。

IDEA警告

5. @Override

IDEA警告

编译出错

Child.sayHello2()虽然会有IDEA警告,但是不会编译出错;Child.sayHello3()因为使用了@Override修饰,但是在父类里面并没有sayHello3()这个方法,所以会编译出错;Child.sayHello1()属于正常使用。

6. @SuppressWarnings

@SuppressWarnings可用于消除警告,可以消除哪些情况下的警告呢,我们举一个例子。

例子图

在这个例子里面

  1. 我们使用了已经废弃的java.util.Date构造方法public Date(int year, int month, int date),所以在编译的时候会出现编译警告。
  2. 因为泛型擦除的原因,往指定泛型插入非泛型类型时会出现警告。
  3. 通过使用@SuppressWarnings,我们可以消除编译期警告

Annotation的作用

我们总结一下Annotation的作用,大概有下面这几种

  1. 编译检查,jdk自带的@SuppressWarnings@Deprecated@Override都有编译检查的功能
  2. 在反射中使用Annotation,这在很多的框架代码里面大量出现,比如Spring、Mybatis、Hibernate等等
  3. 根据Annotation生成帮助文档,通过使用@Documented,我们可以将Annotation也生成到javadoc里面
  4. 能够帮忙查看查看代码,比如通过使用@Override@Deprecated,我们很容易知道我们的集成层级、废弃状态等等

拾遗1 - @SuppressWarnings可以消除哪些警告

不同的编译器,可以消除的警告会有所不同,如果我们使用的是javac编译器,那么我们可以通过命令javac -X来列出所有支持的可以消除的警告。常用的有下面这些

  • deprecation - to suppress warnings relative to deprecation(抑制过期方法警告)
  • rawtypes - to suppress warnings relative to un-specific types when using + generics on class params(使用generics时忽略没有指定相应的类型)
  • unchecked - to suppress warnings relative to unchecked operations(抑制没有进行类型检查操作的警告)
  • unused - to suppress warnings relative to unused code (抑制没被使用过的代码的警告)

拾遗2 - Annotation中的value和default

Annotation的成员变量里面,value很多时候可以简化我们对Annotation的使用。

  1. 当我们使用Annotation的时候,如果只有一个value需要传入的时候,我们省略掉value,即:Element
  2. value声明为数组,使用时需要用大括号括起来+逗号分隔,如{"FirstElement", "SecondElement"}
  3. value声明为数组,使用时只有一个元素时,可当成单元素使用,即:"Element"
public class ValueTest {

    @Documented
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME) @interface OneElement {
        String value();
    }

    @Documented
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME) @interface MultiElement {
        String[] value();
    }

    @Documented
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME) @interface MultiButOnlyInputOneElement {
        String[] value();
    }

    @OneElement("abc")
    @MultiElement({"abc", "cba"})
    @MultiButOnlyInputOneElement("abc")
    class User {
    }
}

同时,在定义Annotation成员变量的时候,我们可以使用default来设置默认值,使用时如果该成员变量的值和默认值相同,则可省略此成员变量的值传入。

@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME) @interface MyAnnotation {
    String value() default "is null";
}

总结

本文,我们首先给出了Annotation的整体结构图,然后分析了Annotation的语法和用法,最后我们给出了一些例子来说明了jdk自带的Annotation的用法,并总结了Annotation的使用场景。同时,我们通过对一些技巧性使用的补充,加深了我们对Annotation的印象。

我有一个微信公众号,经常会分享一些Java技术相关的干货;如果你喜欢我的分享,可以用微信搜索“Java团长”或者“javatuanzhang”关注。

阅读更多

没有更多推荐了,返回首页