Java 中的注解详解(包含Java8新增注解)

  • Post author:
  • Post category:java



目录


1、Java 注解的介绍


2、如何定义一个 Java 注解?


3、在 Java 中预定义的一些注解


4、Java Java SE 8 中新增的类型注解


5、Java Java SE 8 中新增的可重复注解


6、如何解析一个注解?


1、Java 注解的介绍

Java 注解是元数据的一种形式,注解可以用来给应用程序提供数据,不过这些数据并不是程序本身的一部分。注解对所注解的代码逻辑没有直接影响。

注解有很多用途,其中包括:

  • 作为编译器的信息载体——编译器可以使用注解来检测错误或压制警告。
  • 作为编译时和部署时处理的信息载体——软件工具可以处理注解信息,用来生成代码、XML文件等等。
  • 作为运行时处理的信息载体——有些注解可以在运行时进行检查。


注解的本质:

注解的本质就是一个继承了 Annotation 接口的接口,

一个注解只不过是一种特殊的注释而已,如果没有解析它的代码,它连注释都不如


注解可以在哪些地方使用?

注解可以用来声明:类、字段、方法和其他程序元素。一般情况下,按照惯例,每个注解都独占一行。从 Java SE 8 发行版开始,注解除了用作声明外,也可以应用于内置类型检查。下面是一些例子:

// 创建类的实例
new @Interned MyObject();

// 类型转换
myString = (@NonNull String) str;

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

// 抛出异常
void monitorTemperature() throws @Critical TemperatureException { ... }

以上形式的注解称为类型注解。

2、如何定义一个 Java 注解?

如果需要在每个类的主体开始部分提供一些额外的信息,传统上使用注释的做法如下:

public class Generation3List extends Generation2List {

   // Author: swadian2008
   // Date: 3/17/2022
   // Current revision: 6
   // Last modified: 4/12/2022
   // By: swadian2008
   // Reviewers: Alice, Bill, Cindy

   // class code goes here

}

如果需要使用注解来添加相同的信息,那么首先需要定义一个注解,定义注解的语法如下:

@interface ClassPreamble {
   String author();
   String date();
   int currentRevision() default 1;
   String lastModified() default "N/A";
   String lastModifiedBy() default "N/A";
   // 注意数组的使用
   String[] reviewers();
}


注解定义看起来很像是接口的定义

,只不过关键字 interface 前面带有 at 符号(@),@ = AT,表示注解类型,可以理解为注解是一种特殊形式的接口。

// 很像接口,比如最终的应用也需要具体的代码去实现,不然注解就没有任何作用


注解的主体中包含了很多注解元素,它们看起来像方法

,这些元素还可以定义属于自己的默认值。

3、在 Java 中预定义的一些注解

在 Java SE API 中预定义了一些注解。这些注解有的是由 Java 编译器使用,有的用来作为定义其他注解的元注解。

1)

由 Java 编译器使用的预定义注解

比如 @Deprecated,@Override,@SuppressWarnings,@SafeVarargs,@FunctionalInterface 等注解。

// 定义在 java.lang 中的三大内置注解:
@Override          //重写标记:标记注解,编译结束后被丢弃
@Deprecated        //被废除标记:标记注解,编译结束后被丢弃
@SuppressWarnings  //压制编译器检查

@SafeVarargs 注解:当应用于方法或构造函数时,断言代码不会对其可变参数执行不安全的操作。当使用此注解时,与可变参数相关的未检查警告将被压制。

@FunctionalInterface 注解:指示声明的接口是一个函数式接口。

2)

Java 元注解:

应用于其他注解的注解称为元注解。在 java.lang.annotation 包中定义了以下几种元注解:@Target、@Documented、@Retention、@Inherited、@Repeatable


@Target 注解

:用来标记另一个注解,限制注解的使用方式和适用对象,@Target 指定以下元素类型之一作为它的值:

// JDK 19

ElementType.ANNOTATION_TYPE   // 可应用于注解
ElementType.CONSTRUCTOR       // 可应用于构造器   
ElementType.FIELD             // 可应用于字段或属性 
ElementType.LOCAL_VARIABLE    // 可应用于局部变量
ElementType.METHOD            // 可以用于方法级注解
ElementType.PACKAGE           // 可应用于包声明
ElementType.PARAMETER         // 可应用于方法参数
ElementType.TYPE              // 可以用于类


@Documented 注解:

@Documented 注解指示该注解应该被 javadoc 工具进行记录,默认情况下,javadoc 工具不记录注解信息,但声明注解时如果标记了 @Documented,那么被标记的注解就会被 javadoc 工具处理,注解信息也会被记录在生成的文档中。

使用 @Documented 标记的注解生成的文档效果展示如图:


@Retention 注解:

指定注解的生命周期,比如是编译期还是运行时有效:

基本参数如下:

RetentionPolicy.SOURCE:被标记的注解仅保留在源代码级别,并被编译器忽略。

RetentionPolicy.CLASS:被标记的注解在编译时被编译器保留,但被Java虚拟机(JVM)忽略,运行时不使用

RetentionPolicy.RUNTIME:被标记的注解由JVM保留,因此在运行时环境可以使用它。


@Inherited 注解:

指示被标记的注解具有可继承性(默认情况下注解不能从父类继承)。也就是说该注解修饰了一个类,而该类的子类将自动继承父类的注解。该注解仅应用于对的类声明。



代码示例:被 @Inherited 标记的注解具有继承性

/**
 * 注解A——@Inherited标记
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface UserCaseA {
    public String usernameA() default "UserCaseA的默认值";
}

/**
 * 注解B
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface UserCaseB {
    public String usernameB() default "UserCaseB的默认值";
}

/**
 * 父类A
 */
@UserCaseA
public class SuperA {
}

/**
 * 父类B
 */
@UserCaseB
public class SuperB {
}

/**
 * 子类A
 */
public class SubA extends SuperA {
}

/**
 * 子类B
 */
public class SubB extends SuperB {
}

public class Test {
    public static void main(String[] args) {
        // 被 @Inherited 标记的 @UserCaseA
        Class<SubA> subAClass = SubA.class;
        System.out.println("subAClass have @UserCaseA ?:" + subAClass.isAnnotationPresent(UserCaseA.class));
        // 无 @Inherited 标记的 @UserCaseB
        Class<SubB> subBClass = SubB.class;
        System.out.println("subBClass have @UserCaseB ?:" + subBClass.isAnnotationPresent(UserCaseB.class));
    }
}

执行程序后,将输出以下结果:

// 没有被 @Inherited 标记的注解不能被子类继承

subAClass have @UserCaseA ?:true
subBClass have @UserCaseB ?:false


@Repeatable 注解

:在 Java SE 8 中引入的,表示标记的注解可以多次应用于相同的声明或类型。

4、Java Java SE 8 中新增的类型注解

在 Java SE 8 发布之前,注解只能用于声明。从 Java SE 8 发行版开始,注解还可以在使用类型的任何地方使用。比如前面提到的使用类型的几个例子,创建(new)、类型转换、实现语句和异常抛出语句:

// 创建类的实例
new @Interned MyObject();

// 类型转换
myString = (@NonNull String) str;

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

// 抛出的异常声明
void monitorTemperature() throws @Critical TemperatureException { ... }


创建 Java 类型注解的目的,是为了改进 Java 的程序分析,确保更强的类型检查。

Java SE 8 发行版并没有提供类型检查的框架,但是允许开发人员编写(或引入)类型检查框架,这些框架实现了一个或多个可插拔的模块(这些模块可以与 Java 编译器一起使用)。

// 编译期提供更强的类型检查

例如,如果希望确保程序中的某个变量永远不会被赋值为 null;或者想要避免触发NullPointerException。那么可以编写一个自定义插件来检查这一点,然后修改代码注解该变量,表明它永远不会被赋值为 null。变量声明可能是这样的:

@NonNull String str;

当编译代码时(包括在命令行上编译 NonNull 模块),如果编译器检测到潜在的问题,就会会发出警告,该警告可以让你及时的修改代码从而避免产生错误。

合理的使用类型注解和可插拔类型检查器,可以让开发人员编写更加强大、更不容易出错的代码。不过在许多情况下,都不必编写自己的类型检查模块,因为已经有第三方为你做了类似的工作。

// 使用第三方框架

5、Java Java SE 8 中新增的可重复注解

在某些情况下,可能希望将相同的注解应用于声明或类型检查。那么在Java SE 8发行版中,就可以通过重复注解来实现。

例如,想要编写一个计时器,在每月的最后一天和每个周五晚上的 11:00 运行doPeriodicCleanup() 方法。那么就需要创建 @Schedule 注解并将其应用于 doPeriodicCleanup() 方法两次。示例代码如下所示:

@Schedule(dayOfMonth="last")
@Schedule(dayOfWeek="Fri", hour="23")
public void doPeriodicCleanup() { ... }


1)声明一个可重复的注解


//声明一个可重复注解的步骤

注解必须使用 @Repeatable 元注解进行标记。下面的例子定义了一个自定义的 @Schedule可重复注解:

import java.lang.annotation.Repeatable;

@Repeatable(Schedules.class)
public @interface Schedule {
  String dayOfMonth() default "first";
  String dayOfWeek() default "Mon";
  int hour() default 12;
}

@Repeatable() 注解括号中的值,是 Java 编译器为存储重复注解而生成的注解容器的类型。在本例中,包含的注解类型是 Schedules,因此重复的 @Schedule 注解会存储在 @Schedules 注解中。

// Schedules 是注解的容器类型

如果一个注解没有声明它是可重复的,那么就不能在同一个地方重复的使用它,否则将导致编译时错误。


2)声明重复注解的容器注解

容器注解必须包含数组类型的元素,数组类型的元素类型必须是可重复的注解类型。对于包含注解类型 @Schedule 的 @Schedules 的声明如下:

public @interface Schedules {
    Schedule[] value();
}


3)如何查找注解?

Reflection API (反射)中有几个方法可用来查找注解。比如获取单个注解的方法:AnnotatedElement.getAnnotation(Class<T>)。如果存在多个注解,则可以通过获取它们的容器注解来获取它们,比如使用一次返回多个注解的方法: AnnotatedElement.getAnnotationsByType(Class<T>)。

Class 类中提供了以下一些 API 用于获取定义在类,属性或方法上的注解:

public <A extends Annotation> A getAnnotation(Class<A> annotationClass):返回指定的注解

public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass):判定当前元素是否被指定注解修饰

public Annotation[] getAnnotations():返回所有的注解

public <A extends Annotation> A getDeclaredAnnotation(Class<A> annotationClass):返回本元素的指定注解

public Annotation[] getDeclaredAnnotations():返回本元素的所有注解,但不包括父类继承而来的

6、如何解析一个注解?

解析注解的两种方式:

  1. 编译期扫描——适用于JDK内置的几个注解
  2. 运行期反射——适用于自定义注解

接下来主要介绍通过运行期反射来获取自定义注解的操作。


1)首先定义一个注解

自定义注解可以选择性的使用元注解进行修饰,这样可以更加具体的指定注解的生命周期、作用范围等信息

@Target(ElementType.METHOD) // 作用于方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时有效,通过反射可以获取注解的配置值
public @interface UserCase {// 注解就是一个标签,使用@interface定义
    /**
     * 注解定义元素的方式,可以设置默认值
     * @return
     */
    public String id();
    public String descrption() default "no descrption";
}


2)通过反射获取注解的值



通过获取类的字节码,封装为 Class 类,从而可以使用 Class 类中的方法:

public class AnnotationTest {

    /**
     * 使用注解作用在方法上
     * @param password
     * @return
     */
    @UserCase(id = "8888",descrption = "替代默认值")
    public String getPassword(String password){
        return password;
    }

    /**
     * 通过反射获取默认值
     * @param args
     */
    public static void main(String[] args) {
        // 获取类的字节码
        Class<AnnotationTest> cl = AnnotationTest.class;
        // 获取类中所有的方法,包括私有方法
        Method[] declaredMethods = cl.getDeclaredMethods();
        if(Objects.nonNull(declaredMethods)){
            for(Method method : declaredMethods){
                // 判断方法上是不是包含有指定的注解
                if(method.isAnnotationPresent(UserCase.class)){
                    // 获取方法上的指定注解
                    UserCase annotation = method.getAnnotation(UserCase.class);
                    if(Objects.nonNull(annotation)){
                        // 获取并打印注解的配置信息
                        System.out.println(annotation.id()+":"+annotation.descrption());
                    }
                }
            }
        }
    }
}

上述程序的执行结果:

至此,Java 中的注解介绍完毕。



版权声明:本文为swadian2008原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。