这篇文章将深入的介绍 Java 的注解类型方面的知识 |
理解注解
☕️ 什么是注解
官方给出的定义是:注解是一种能被添加到 java 代码中的元数据,类、方法、变量、参数和包都可以用注解来修饰,注解对于它所修饰的代码并没有直接的影响。
所以我可以知道:
- 注解是一种元数据形式,即注解是属于 java 的一种数据类型,和类、接口、数组、枚举类似
- 注解可以用来修饰类、方法、变量、参数和包
- 注解不会对所修饰的代码产生直接的影响
☕️ 注解的使用范围
官方给出的使用范围的描述是:注解又许多用法,包括为编译器提供信息 - 注解能被编译器检测到错误或抑制警告、编译和部署时的处理 - 软件工具能处理注解信息从而生成代码或XML文件等等、运行时的处理 - 有些注解在运行时能被检测到。
☕️ 注解基本语法
注解类型的声明
注解在 Java 中,与类、接口、枚举类似,因此其声明语法基本一致,使用的关键字为:@interface,在底层实现上,所有定义的注解都会自动继承 java.lang.annotation.Annotation 接口:
public @interface MyAnnotation {
}
注解类型的实现
根据我们在自定义类的经验,在类的实现部分无非就是书写构造、属性或方法。但是,在自定义注解中,其实现部分只能定义一个东西:注解类型元素(annotation type element):
public @interface MyAnnotation {
public String name();
int age() default 18;
int[] array();
}
定义注解类型元素时需要注意如下几点:
- 访问修饰符必须为public,不写默认为public
- 该元素的类型只能是基本数据类型、String、Class、枚举类型、注解类型(体现了注解的嵌套效果)以及上述类型的一种数组
- 该元素的名称一般定义为名词,如果注解中只有一个元素,请把名字起为 value
- ( )不是定义方法参数的地方,不能在括号中定义任何参数,它仅仅只是一个特殊的语法而已
- default 代表默认值,值必须和定义的类型一致
- 如果没有默认值,则表示后续使用注解时必须给该类型元素赋值
元注解
一个最基本的注解定义就只包括两部分内容:1、注解的名字;2、注解包含的类型元素。但是,我们在使用JDK自带注解的时候发现,有些注解只能写在方法上面(比如@Override),有些却可以写在类的上面(比如@Deprecated)等等,这些是如何规定的呢?答案就是我们的元注解
元注解:专门修饰注解的注解。它们都是为了更好的设计自定义注解的细节而专门设计的,下面介绍一些常用的元注解。
📝 @Target
@Target 注解,是专门用来限定某个自定义注解能够被应用在哪些Java元素上面的。它使用一个枚举类型来定义:
public enum ElementType {
/* 类,接口(包括注解类型)或枚举的声明 */
TYPE,
/* 属性的声明 */
FIELD,
/* 方法的声明 */
METHOD,
/* 方法形式参数声明 */
PARAMETER,
/* 构造方法的声明 */
CONSTRUCTOR,
/* 局部变量声明 */
LOCAL_VARIABLE,
/* 注解类型声明 */
ANNOTATION_TYPE,
/* 包的声明 */
PACKAGE
/* 类型参数声明: JDK 1.8 之后 */
TYPE_PARAMETER,
/* 使用一种类型: JDK 1.8 之后 */
TYPE_USE,
/* 模块声明: JDK 9 之后 */
MODULE
}
比如我们定义一个只能使用在类、接口或方法上面的注解:
@Target(value = {ElementType.TYPE, ElementType.METHOD})
public @interface MyAnnotation {
String name();
int age() default 18;
int[] array();
}
📝 Retention
@Retention 注解,翻译为持久力、保持力,用来修饰自定义注解的生命周期
注解的生命周期有三个阶段:1、Java 源文件阶段;2、编译到 class 文件阶段;3、运行期阶段。使用 RetentionPolicy 枚举类型定义了三个阶段:
public enum RetentionPolicy {
/**
* @源文件阶段:
* 注解将被编译器忽略掉
*/
SOURCE,
/**
* @ 编译到 class 文件阶段:
* 注解将被编译器记录在class文件中,但在运行时不会被虚拟机保留,这是一个默认的行为
*/
CLASS,
/**
* @运行期阶段:
* 注解将被编译器记录在class文件中,而且在运行时会被虚拟机保留,因此它们能通过反射被读取到
*/
RUNTIME
}
我们把 @Retention 注解总结为以下几点:
- 如果一个注解被定义为 RetentionPolicy.SOURCE,则它将被限定在 Java 源文件中,那么这个注解即不会参与编译也不会在运行期起任何作用,这个注解就相当于注释一样的效果,只能被阅读 Java 文件的人看到
- 如果一个注解被定义为 RetentionPolicy.CLASS,则它将被编译到 Class 文件中,那么编译器可以在编译时根据注解做一些处理动作,但在运行时,JVM(Java虚拟机)会忽略它,我们在运行期也不能读取到
- 如果一个注解被定义为 RetentionPolicy.RUNTIME,那么这个注解可以在运行期的加载阶段被加载到 Class 对象中。并且在程序运行阶段,我们可以通过反射得到这个注解,并通过判断是否有这个注解或这个注解中属性的值,从而执行不同的程序代码段。我们实际开发中的自定义注解几乎都是使用的 RetentionPolicy.RUNTIME
自定义注解
从上面的讲解介绍中我们已经知道:注解其实就是一种标记,可以在程序代码中的关键节点(类、方法、变量、参数、包)上打上这些标记,然后程序在编译时或运行时可以检测到这些标记从而执行一些特殊操作。因此可以得出自定义注解使用的基本流程:
- 第一步:定义注解,相当于定义标记
- 第二步:配置注解,把标记打在需要用到的程序代码中
- 第三步:解析注解,在编译期或运行时检测到标记,并进行特殊操作
我们通过一个例子来讲解如何自定义注解并在具体的类上使用注解:
// 定义一个用在方法上的注解
@Target(value = {ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyAnnotation {
String name();
int age() default 18;
int[] score();
}
// 定义一个测试注解的类
public class Student {
@MyAnnotation(name = "Andy", age = 24, score = {88, 95, 89})
public void study(int times) {
for (int i = 0; i < times; i++) {
System.out.println("Good Good Study, Day Day Up!");
}
}
}
我们分析一下这个例子:
- 1、MyAnnotation 的 @Target 定义为 ElementType.METHOD,那么它书写的位置应该在方法定义的上方,即:public void study(int times) 的上面
- 2、由于我们在 MyAnnotation 中定义的有注解类型元素,而且有些元素是没有默认值的,所以这要求我们在使用的时候必须在标记名后面打上(),并且在()内以"元素名=元素值"的形式挨个填上所有没有默认值的注解类型元素(有默认值的也可以填上重新赋值),中间用逗号","号分割
注解与反射机制
为了运行时能准确获取到注解的相关信息,Java 在 java.lang.reflect 反射包下新增了 AnnotatedElement 接口,它主要用于表示目前正在 VM 中运行的程序中已使用注解的元素,通过该接口提供的方法可以利用反射技术地读取注解的信息,如反射包的 Constructor类、Field类、Method类、Package类和Class类都实现了 AnnotatedElement 接口,它简要含义如下:
- Class:f类的Class对象定义
- Constructor:代表类的构造器定义
- Field:代表类的成员变量定义
- Method:代表类的方法定义
- Package:代表类的包定义
下面是 AnnotatedElement 中相关的一些 API 方法,以上5个类都实现以下的方法:
返回值 | 方法名称 | 功能说明 |
---|---|---|
< T extends Annotation> T | getAnnotation(Class annotationClass) | 该元素如果存在指定类型的注解,则返回这些注解,否则返回 null |
Annotation[] | getAnnotations() | 返回此元素上存在的所有注解,包括从父类继承的 |
boolean | isAnnotationPresent(Class<? extends Annotation> annotationClass) | 如果指定类型的注解存在于此元素上,则返回 true,否则返回 false |
Annotation[] | getDeclaredAnnotations() | 返回直接存在于此元素上的所有注解(不包括父类的注解),调用者可以随意修改返回的数组;这不会对其他调用者返回的数组产生任何影响,没有则返回长度为0的数组 |
如果想要了解更多相关 API 方法,可以去 JDK 源码中查看。下面我们通过一个案例来了解一下如何使用反射获取注解的相关信息:
// 定义注解 DocumentA
@Inherited
@Documented
@Target(value = {ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DocumentA {
}
// 定义注解 DocumentB
@Documented
@Target(value = {ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface DocumentB {
}
// 定义父类 Person 并使用注解 DocumentA 修饰
@DocumentA
public class Person {
}
// 定义子类 Student 继承 Person 类,并用注解 DocumentB 修饰
@DocumentB
public class Student extends Person {
public static void main(String[] args) {
Class<?> clazz = Student.class;
// 获取指定注解
DocumentA documentA = clazz.getAnnotation(DocumentA.class);
System.out.println("documentA: " + documentA);
// 获取所有注解,包括从父类继承下来的注解
Annotation[] allAnn = clazz.getAnnotations();
System.out.println("all annotation: " + Arrays.toString(allAnn));
// 获取所有注解,但不包括从父类继承下来的注解
Annotation[] allDeclAnn = clazz.getDeclaredAnnotations();
System.out.println("all declared annotation: " + Arrays.toString(allDeclAnn));
// 判断是否包含注解 DocumentA
boolean b = clazz.isAnnotationPresent(DocumentA.class);
System.out.println("包含 DocumentA: " + b);
}
}
>>>>>
documentA: @com.my.main.DocumentA()
all annotation: [@com.my.main.DocumentA(), @com.my.main.DocumentB()]
all declared annotation: [@com.my.main.DocumentB()]
包含 DocumentA: true
这里我们需要注意的是:父类 Person 的注解 DocumentA 想要被子类 Student 继承的话,需要在注解 DocumentA 中添加元注解 @Inherited 来表明注解 DocumentA 能够被子类继承,否则子类无法继承该注解。
下面我们通过一个例子来讲解如何通过反射来获取注解类型元素:
// 定义一个用在方法上的注解
@Target(value = {ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyAnnotation {
String name();
int age() default 18;
int[] score();
}
public class Student {
@MyAnnotation(name = "Andy", age = 24, score = {88, 95, 89})
public void study(int times) {
for (int i = 0; i < times; i++) {
System.out.println("Good Good Study, Day Day Up!");
}
}
public static void main(String[] args) {
// 获取 Student 的 Class 对象
Class<?> clazz = Student.class;
try {
// 获取 Method 对象
Method method = clazz.getMethod("study", int.class);
// 获取注解类型元素
if (method.isAnnotationPresent(MyAnnotation.class)) {
System.out.println("Student 的 study 方法上包含了 MyAnnotation 注解");
// 打印注解类型元素
MyAnnotation anno = method.getAnnotation(MyAnnotation.class);
System.out.println("name: " + anno.name() + "age: " + anno.age() + "score: "
+ Arrays.toString(anno.score()));
} else {
System.out.println("Student 的 study 方法上没有 MyAnnotation 注解");
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
}
>>>>>
Student 的 study 方法上包含了 MyAnnotation 注解
name: Andyage: 24score: [88, 95, 89]