Java 注解(Annotation)的基础知识

  • 注解(Annotaion )
    • 注解的形式
    • 注解的分类
      • 自定义注解&注解声明
      • 预定义注解
        • 元注解
        • 标准注解
    • 类型注解(Type Annotations) &可插拔式系统(Pluggable Type Systems)

注解(Annotaion )

Annotations, a form of metadata, provide data about a program that is not part of the program itself. Annotations have no direct effect on the operation of the code they annotate. -----官方文档

注解(也被成为元数据)为我们在代码中添加信息提供了一种形式化的方法,使我们可以在稍后某个时刻非常方便地使用这些数据。 ———摘自《Thinking in Java》

即:

  1. 注解是一种元数据
  2. 注解提供提供了一种形式化的方法让我们在代码中添加信息(但这个信息不属于程序本身),需要我们稍后再对这些数据进行处理。
  3. 注解不对其注解的代码产生直接影响。

元数据(metadata):描述数据的数据。

对于”直接影响”,让我们先看看注解的作用

  • Information for the compiler — Annotations can be used by the compiler to detect errors or supprhuess warnings.
  • Compile-time and deployment-time processing — Software tools can process annotation information to generate code, XML files, and so forth.
  • Runtime processing — Some annotations are available to be examined at runtime.

即:

  • 为编译器提供信息 ——被编译器用来检测错误或者‘压制’警告
  • 编译时和部署时处理——软件工具可以处理注释信息以 生成代码
  • 运行时处理——某些注释可以在程序运行期间被检测到

前文提到过 注解不对其注解的代码产生直接影响,那么注解是怎么起到以上的作用的呢?
——注解处理器(Annotation Processor):于编译期间发挥作用
——利用反射机制:于程序运行期间发挥作用

如果不对注解进行处理,那么注解就和注释相差不大。



注解的形式

注解的形式很简单,如:
@Override

	@Override
	void myrMethod() { ... }

符号@ 告诉编译器紧接其后的是一个注解。

注解可以包含元素(需要声明中有定义)

	@Author(
	  name = "Bob",
	  date = "12/22/2020"
	  )
	  class MyClass() { ... }

当注解只有一个元素(元素名必须为 value ),则元素名可以省略。当元素类型为数组且数组长度为1,同样可以省略。

@Target(ElementType.METHOD)
@SuppressWarnings("unchecked")
void myMethod() { ... }

若注解没有元素,则可以省略括号,就像@Override的使用一样

在同一个地方可以使用多个不同的注解

@Author(name = "Jane Doe")
@EBook
class MyClass { ... }

若要使用多个相同的注解,需要用到 容器 知识,见后文。



注解的分类

根据注解参数的个数,我们可以将注解分为三类:

  1. 标记注解:一个没有成员定义的Annotation类型被称为标记注解。这种Annotation类型仅使用自身的存在与否来为我们提供信息。比如后面的系统注解@Override;
  2. 单值注解
  3. 多值注解

根据注解使用方法和用途,我们可以将Annotation分为三类:

  1. 预定义注解
  2. 自定义注解

下面列出预定义注解 表格(以参数个数划分)

标记注解 单值注解 多值注解
@Override @Retention
@SafeVarargs @SuppressWarnings @Deprecated
@FunctionalInterface @Target
@Documented @Repeatable
@Inherited

自定义注解&注解声明

定义注解格式
  public @interface 注解名 {定义体}

  • @interface 用来声明一个注解,以声明无形参方法的形式声明元素。
  • 注解元素的可支持数据类型:
     1. 所有基本数据类型int,float,boolean,byte,double,char,long,short)
     2. String 类型
     3. Class 类型
     4. enum 类型
     5. Annotation 类型
     6. 以上所有类型的一维数组
package com.example.test.annotationtest;

@interface T {
    String value();
}
/**
 * @author wzq20
 */
public @interface Test {
	//枚举类型
    enum Status {Y,N};

    //声明枚举
    Status status() default Status.Y;

    //布尔类型
    boolean bool() default false;
	
	// 基本数据类型
	int id() default -1;

    //String类型
    String name()default "";

    //class类型
    Class<?> clazz() default int.class;

    //注解嵌套
    T t() default @T("wzq");

    //数组类型
    long[] value();
}
  • 编译器对默认值有限制

    • 元素不能有不确定的值。也就是说,元素必须要么具有默认值,要么在使用注解时提供元素的值。
    • 对于非基本类型的元素,无论是在使用时赋值,还是在注解接口中定义默认值,都不能以null作为值。这就造成 每一个元素都存在,无法用 null 表示某个元素不存在。因此,为了绕开这个限制,只能定义一些特殊的值,例如 空字符串负数 ,表示某个元素不存在。
  • 只能用public修饰或缺省.
    Java 注解(Annotation)的基础知识_第1张图片

  • 使用 @interface 自定义注解时,自动继承了 java.lang.annotation.Annotation 接口,由编译程序自动完成其他细节。
    反编译 后得到:

package com.example.test.annotationtest;

import java.lang.annotation.Annotation;

public interface Test extends Annotation{
	...
}

可以看出注解实际上就是一种接口

  • 在定义注解时,不能继承其他的注解或接口。(尽管java接口支持多继承)
    Java 注解(Annotation)的基础知识_第2张图片


预定义注解

已经预先定义在Java SE API中的注解类型。

  • 其中一些被Java 编译器使用,被定义在包java.lang中,包括 @Deprecated, @Override, @SuppressWarnings@SafeVarargs@FunctionalInterface,又被称作标准注解(其中后两个注解在Java SE 8时新增)
  • 另外一些作用于其他注解的注解,被定义在包java.lang.annotation.中,又被称为元注解

元注解

@Retention

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
    RetentionPolicy value();
}

该注解指定如何存储以标记的注解

  • RetentionPolicy.SOURCE —— 注解将被编译器丢弃(该类型的注解信息只会保留在源码里,源码经过编译后,注解信息会被丢弃,不会保留在编译好的class文件里)
  • RetentionPolicy.CLASS —— 注解在class文件中可用,但会被JVM丢弃(该类型的注解信息会保留在源码里和class文件里,在执行的时候,不会加载到虚拟机中)。
  • RetentionPolicy.RUNTIME —— 注解信息将在运行期(JVM)也保留,因此可以通过反射机制读取注解的信息(源码、class文件和执行的时候都有注解的信息)

为了更清楚上述三个类型的含义,先看看一个Java程序的运行过程

Java程序运行过程

一个Java程序从源代码到运行可以简单分为两个过程:

  1. 编译器将源代码编译成字节码
  2. JVM将字节码解释运行

The Java programming language compiler, javac, reads source files written in the Java programming language, and compiles them into bytecode class files. Optionally, the compiler can also process annotations found in source and class files using the Pluggable Annotation Processing API. The compiler is a command line tool but can also be invoked using the Java Compiler API. ——官方文档

通过官方文档Java Complier,javac,我们看到,java编译器还具有处理注解的功能,下面简单说一下java程序运行过程中,java编译器和JVM对java注解的处理。

Java源代码先经过Java编译器编译,此过程,类型为 RetentionPolicy.SOURCERetentionPolicy.CLASS 的注解会被注解处理器(Annotation Processor) 处理。类型为 RetentionPolicy.CLASS 的注解会被编译为字节码在.class文件内继续存在,而类型为 RetentionPolicy.SOURCE 的注解不会。
前文提到过注解的两个作用:

  • 为编译器提供信息 ——被编译器用来检测错误或者‘压制’警告
  • 编译时和部署时处理——软件工具可以处理注释信息以 生成代码

都是通过注解处理器(Annotation Processor) 处理后发挥的作用,详情可见ANNOTATION PROCESSING 101。

字节码文件进入JVM后,解释器会把类型为 RetentionPolicy.RUNTIME 解释为机器代码,忽略类型为 RetentionPolicy.CLASS 的注解。最后运行时可以通过反射处理类型为 RetentionPolicy.RUNTIME 的注释。


@Documented

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {
}

该注释表示,每当使用指定的注解时,都应使用Javadoc工具记录这些元素。 (默认情况下,Javadoc中不包含注解。)有关更多信息,请参见Javadoc工具页面。
@Documented 标记的注解会在Javadoc里出现。


@Target

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    ElementType[] value();
}

该注释标记另一个注解,以限制该注释可以应用于哪种类型的Java元素。 @Target 注解将以下元素类型之一指定为其值

  • ElementType.ANNOTATION_TYPE 用于注解类型
  • ElementType.CONSTRUCTOR 用于构造器
  • ElementType.FIELD 用于实例域
  • ElementType.LOCAL_VARIABLE 用于局部变量
  • ElementType.METHOD 用于方法
  • ElementType.PACKAGE can 用于包声明
  • ElementType.PARAMETER 用于方法参数
  • ElementType.TYPE 用于接口(包括注解),类和枚举类型。
  • ElementType.TYPE_USE 可以被用在任何类型使用的地方。
    (Java 8新加入)
  • ElementType.TYPE_PARAMETER 用于类型变量(注:即泛型)的声明中(比如 MyClass<@Test T> )(Java 8新加入)

ElementType.TYPE_USEElementType.TYPE_PARAMETER 于Java 8新增
ElementType.TYPE_USE 的具体使用可见后文 类型注解

默认情况下(未指明Target),注解可以用在任意位置。
若只有一个值,则可以省略value。若有多个值,则以{}包含并用逗号隔开。

@Target(ElementType.METHOD)
@Target(value={ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.LOCAL_VARIABLE})

@Inherited

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {
}

该注解表示可以从超类继承注解类型。 (默认情况下,这是不正确的。)当用户查询注解类型并且该类没有该类型的注解时,将在该类的超类中继续查询。 该注解仅适用于类声明。例如:

/**
 * @author wzq20
 */
public class AnnotationTest {
    public static void main(String[] args) {
        A a = new A();
        B b = new B();
        C c = new C();
        D d = new D();
        System.out.println("A " + a.getClass().isAnnotationPresent(Inheritable.class));
        System.out.println("B " + b.getClass().isAnnotationPresent(Inheritable.class));
        System.out.println("C " + c.getClass().isAnnotationPresent(NotInheritable.class));
        System.out.println("D " + d.getClass().isAnnotationPresent(NotInheritable.class));
    }
}
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@interface Inheritable {
}

@Retention(RetentionPolicy.RUNTIME)
@interface NotInheritable {
}

@Inheritable
class A {
}

class B extends A {
}

@NotInheritable
class C {
}

class D extends C {
}

运行结果:
Java 注解(Annotation)的基础知识_第3张图片


@Repeatable

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Repeatable {
    Class<? extends Annotation> value();
}

在Java SE 8中引入的 @Repeatable 注解表示标记的注解可以多次应用于相同的声明或类型使用。下面举一个例子,现在我们想要用一个注解来表明一个人的身份,于是我们写了以下代码:

@interface Person{
    String role() default "";
}

/**
	超人是一个程序员
*/
@Person (role = "coder")
public class SuperMan{
}

可是如果超人有多个身份呢?可能有人记起了前文 在同一个地方可以使用多个不同的注解 ,可能会这样写

@Person(role="artist")
@Person(role="coder")
@Person(role="PM")
public class SuperMan{
}

然后就会发现程序报错了,IDEA提醒我们 Person 需要用 @Repeatable 注解标记,现在就到了 @Repeatable 注解发挥作用的时候了。
Java 注解(Annotation)的基础知识_第4张图片
使用 @Repeatable 注解的形式是

@Repeatable(容器名.class)

我们已经声明了想要重复标记的注解 @Person,所以先需要声明一个包含@Person注解的一个 容器,我们取名为@Persons。容器注解必须包含一个名为value,类型为可重复注解的元素:示例为:

@interface Persons {
    Person[]  value();
}

完整示例为:

@interface Persons {
    Person[]  value();
}

@Repeatable(Persons.class)
@interface Person{
    String role() default "";
}
// java 8 后
@Person(role="artist")
@Person(role="coder")
@Person(role="PM")
public class SuperMan{
}

但在没有引入 @Repeatable 注解前,Java实现相同重复注解的功能是直接通过 容器注解完成的

//java 8前
@Pernsons(value = {
@Person(role="artist"),
@Person(role="coder"),
@Person(role="PM") 
} )
public class SuperMan{
}

实际上 @Repeatable 原理都是利用了 容器 注解
对代码进行反编译

 // java 8 后
@Person(role="artist")
@Person(role="coder")
@Person(role="PM")
public class SuperMan{
}

得到:

@Persons({@Person(
    role = "artist"
), @Person(
    role = "coder"
), @Person(
    role = "PM"
)})
class SuperMan {
    SuperMan() {
    }
}

可以发现@Repeatable注解其实就是一个 语法糖1 ,实际上还是用 容器 实现的。

现在我们再来考虑一个问题: **如果一个可重复注解只标记的目标一次,那么Java还是会用容器注解完成吗?
对代码进行反编译

@Person (role = "coder")
public class SuperMan{
}

得到:

@Person(
    role = "artist"
)
class SuperMan {
    SuperMan() {
    }
}

发现还是进行了一些优化,直接使用了@Person注解而不是@Persons容器类注解。


标准注解

@Deprecated

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, MODULE, PARAMETER, TYPE})
public @interface Deprecated {
    String since() default "";
	forRemoval() default false;
}

该注解(作用于类,方法或实例域)表明被标记的部分是废弃的、应该不再使用的。当使用被该注解标记过的方法,类或者实例域时,编译器会生成警告。如果一个元素是废弃的,那么应该用Javadoc 的@depreated记录下来,并且指出为什么废弃,若有替代,应该指出用什么替代。例如

public interface House { 
    /**
     * @deprecated use of open 
     * is discouraged, use
     * openFrontDoor or 
     * openBackDoor instead.
     */
    @Deprecated
    public void open(); 
    public void openFrontDoor();
    public void openBackDoor();
}

@Override

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

该注解(作用于方法)告知编译器,这个方法应该覆盖了其超类中的一个方法(或是其实现的接口中的方法)尽管覆盖方法时并不强制要求使用 @Override ,但是使用它有助于避免错误。如果被 @Override标记的方法没有覆盖超类的方法,编译器会生成一个错误。


@SuppressWarnings

@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, MODULE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
    String[] value();
}

该注解告知编译器抑制可能产生的警告。
在下面的例子中,一个废弃的方法被使用了,编译器本该生成一个警告,然而使用了 @SuppressWarnings(“deprecation”) 后该警告会被抑制。

   // use a deprecated method and tell 
   // compiler not to generate a warning
   @SuppressWarnings("deprecation")
    void useDeprecatedMethod() {
        // deprecation warning
        // - suppressed
        objectOne.deprecatedMethod();
    }

The Java Language Specification 中将编译警告分为两类:deprecationunchecked。当与泛型出现之前编写的遗留代码交互时,可能会出现警告 unchecked
要禁止多种类型的警告,请使用以下语法。

@SuppressWarnings({"unchecked", "deprecation"})

NOTE: 编译器可以提供新的类型警告,如IDEA 就提供了一些新警告类型。具体操作可见此处
Java 注解(Annotation)的基础知识_第5张图片


@SafeVarargs

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD})
public @interface SafeVarargs {}

@SafeVarargs 注解应用于方法或构造函数时,它断言代码不会对其varargs参数执行潜在的不安全操作。当使用此注解类型时,与可变参数使用有关的 unchecked 警告将被抑制。一般出现于拥有可变参数的泛型方法中。如下:

public static <T> void addAll(Collections coll, T... ts)
{
for (t : ts) coll.add⑴;
}

应该记得,实际上参数 ts 是一个数组, 包含提供的所有实参。
现在考虑以下调用:

Col1ection<Pair<String» table = . . .;
Pair<String> pairl = . . .;
Pair<String> pair2 = . . .;
addAll(table, pairl, pair2);

为了调用这个方法,Java 虚拟机必须建立一个 Pair< String > 数组, 这就违反了规则(Java中无法创建参数化类型的数组)。不过,对于这种情况, 规则有所放松,你只会得到一个警告,而不是错误。
可以采用两种方法来抑制这个警告。一种方法是为包含 addAll 调用的方法增加注解 @SuppressWamings(“unchecked”) 或者在 Java SE 7中还可以用 @SafeVarargs 直接标注 addAll 方法:

@SafeVarargs
public static <T> void addAll(Collection<T> coll, T... ts)

现在就可以提供泛型类型来调用这个方法了。对于所有只需要读取参数数组元素的方法,都可以使用这个注解。


@FunctionalInterface
该注解作用于接口,表明该接口应该是一个 函数式接口。若标记的接口不是 函数式接口,则编译器会生成一个错误。

NOTE:
函数式接口 :只含有一个抽象方法的接口。



类型注解(Type Annotations) &可插拔式系统(Pluggable Type Systems)

类型注解是一种可以放在 任何使用类型 的位置上的注释(注:在 Java 8 之前的版本中,只能在声明式前使用注解)。这包括 new 运算符类型转换implements 子句和 throws 子句。类型注解加强了分析Java代码的能力,并能够确保更强大的类型检查。

以下大多数示例都来自 Type Annotations Specification。和各种 Java 8 演示文稿。
带有类型注解的简单类型定义如下所示

@NotNull String str1 = ...
@Email String str2 = ...
@NotNull @NotBlank String str3 = ...

用于嵌套类型

Map.@NonNull Entry = ...

构造函数

new @Interned MyObject()
new @NonEmpty @Readonly List<String>(myNonEmptyStringSet)
myObject.new @Readonly NestedClass()

类型转换

myString = (@NonNull String) myObject;
query = (@Untainted String) str;

继承

class UnmodifiableList<T> implements @Readonly List<T> { ... }

泛型

List<@Email String> emails = ...
List<@ReadOnly @Localized Message> messages = ...
Graph<@Directional Node> directedGraph = ...
Map<@NonNull String, @NonEmpty List<@Readonly Document>> documents;

包括参数边界和通配符边界

class Folder<F extends @Existing File> { ... }
Collection<? super @Existing File> c = ...
List<@Immutable ? extends Comparable<T>> unchangeable = ...

抛出异常

void monitorTemperature() throws @Critical TemperatureException { ... }
void authenticate() throws @Fatal @Logged AccessDeniedException { ... }

instanceof 语句

boolean isNonNull = myString instanceof @NonNull String;
boolean isNonBlankEmail = myString instanceof @NotBlank @Email String;

以及最后 Java 8 的方法引用

@Vernal Date::getDay
List<@English String>::size
Arrays::<@NonNegative Integer>sort

创建 类型注释(Type Annotations) 是为了支持对Java程序的改进分析,以确保更强的类型检查。Java SE 8发行版不提供类型检查框架,但允许我们编写(或下载)类型检查框架,该框架被实现为与Java编译器结合使用的一个或多个可插拔模块。

例如,如果要确保程序中的特定变量永远不会被分配为null。需要避免触发 NullPointerException 。可以编写一个自定义插件进行检查。然后,您将修改代码以注释该特定变量,指示该变量从未分配为null。变量声明可能如下所示:

@NonNull String str;

当编译代码时,如果编译器检测到潜在问题,则会打印一条 警告 ,允许我们修改代码以避免 错误 。在更正代码以删除所有 警告 之后,该程序运行时将不会发生此 特定错误。

我们可以使用多个类型检查模块,其中每个模块检查不同类型的错误。这些模块可以在Java类型系统的基础上构建,以在需要的时间和位置添加特定的检查。

通过明智地使用类型注释和可插入类型检查器,可以编写更强大且更不易出错的代码。

在许多情况下,不必编写自己的类型检查模块。可以利用第三方编写的框架。如 Checker Framework


参考 :
Lesson: Annotations
Java 8 Type Annotations
深入理解Java注解类型(@Annotation)
java8注解@Repeatable使用技巧
Type Annotations Specification (JSR 308)
ANNOTATION PROCESSING 101
秒懂,Java 注解 (Annotation)你可以这样学
How compiler deals with annotations?


  1. 语法糖(Syntactic Sugar),也称糖衣语法,是由英国计算机学家 Peter.J.Landin 发明的一个术语,指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。Java 中最常用的语法糖主要有泛型、变长参数、条件编译、自动拆装箱、内部类等。虚拟机并不支持这些语法,它们在编译阶段就被还原回了简单的基础语法结构,这个过程成为解语法糖。 ↩︎

你可能感兴趣的:(Java,java,annotations,编程语言,经验分享)