Java进阶 | 初识注解

简介

Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制。 Java 语言中的类、方法、变量、参数和包等都可以被标注。 注解是元数据的一种形式,提供有关于程序但不属于程序本身的数据。注解对它们注解的代码的操作没有直接影响。

注解的结构

  1. 一个Annotation和一个RetentionPolicy关联
  2. 一个Annotation和一个或者多个ElementType关联
  3. Annotation有许多实现类

Annotation 的源码组成

从Java的源码上看Annotation的组成,有三个重要的主干类: Annotation.java

java 复制代码
package java.lang.annotation;
public interface Annotation {
    /** 判断指定对象与当前注解是否逻辑一致 */
    boolean equals(Object obj);
    /** 计算并返回当前注解的哈希码  */
    int hashCode();
    /** 返回当前注解的字符串表示  */
    String toString();
    /** 返回当前注解对应的注解类型,避免因注解实现类依赖问题,能可靠获取注解类型。 */
    Class<? extends Annotation> annotationType();
}

ElementType.java

java 复制代码
package java.lang.annotation;
public enum ElementType {
    /** 类、接口(包括注解类型)、枚举或记录声明 */
    TYPE,
    /** 字段声明(包括枚举常量) */
    FIELD,
    /** 方法声明 */
    METHOD,
    /** 形式参数声明 */
    PARAMETER,
    /** 构造函数声明 */
    CONSTRUCTOR,
    /** 局部变量声明 */
    LOCAL_VARIABLE,
    /** 注解类型声明 */
    ANNOTATION_TYPE,
    /** 包声明 */
    PACKAGE,
    /** 类型参数声明 */
    TYPE_PARAMETER,
    /** 类型的使用 */
    TYPE_USE,
    /** 模块声明 */
    MODULE,
    /** 记录组件 */
    RECORD_COMPONENT;
}

RetentionPolicy.java

java 复制代码
package java.lang.annotation;
public enum RetentionPolicy {
    /** 注解仅在源码阶段存在,编译器会将其丢弃,不会保留在编译后的字节码文件中。 */
    SOURCE,
    /** 注解会被编译器记录在字节码文件中,但在运行时虚拟机不需要保留这些注解。这是注解的默认保留策略。 */
    CLASS,
    /** 注解会被编译器记录在字节码文件中,并且在运行时会被虚拟机保留,因此可以通过反射机制读取这些注解。 */
    RUNTIME
}

从源码中可以看出:

  1. 每个 Annotation 对象,都会有唯一的 RetentionPolicy 属性;而 ElementType 属性,则有 1~n 个。

  2. ElementType 是 Enum 枚举类型,它用来指定 Annotation 的应用类型。当 Annotation 与某个 ElementType 关联时,就意味着:Annotation有了某种用途。 例如,若一个 Annotation 对象是 METHOD 类型,则该 Annotation 只能用来修饰方法。

  3. RetentionPolicy 是 Enum 枚举类型,它用来指定 Annotation 的作用域。 (1) 若 Annotation 的类型为 SOURCE,Annotation 仅存在于编译器处理期间 ,编译器处理完之后,该 Annotation 就没用了。 例如," @Override" 标志就是一个 Annotation。当它修饰一个方法的时候,就意味着该方法覆盖父类的方法;并且在编译期间会进行语法检查。编译器处理完后,"@Override" 就没有任何作用了。 (2) 若 Annotation 的类型为 CLASS,编译器将 Annotation 存储于类对应的 .class 文件中,它是 Annotation 的默认行为。 (3) 若 Annotation 的类型为 RUNTIME,编译器将 Annotation 存储于 class 文件中,并且可由JVM读入。

注解的类型

Java内置了多个注解,其中3个在java.lang中(如@Override),其余如@Target、@Retention等位于java.lang.annotation包

用于代码的注解

@Override:只能用于方法。表示覆盖父类的方法。如果发现其父类,或者引用的接口中并没有该方法时,会报编译错误。

@Deprecated:标记过时方法。如果使用该方法,会警告。

@SuppressWarnings:忽略警告。

用于注解的注解(元注解)

注解类也能够使用其他的注解声明。 对注解类型进行注解的注解类,我们称之为meta-annotation (元注解)。一般的,我们在定义自定义注解时,需要指定的元注解有两个 :@Target@Retention

@ Target:限制这个注解应该用于哪种 Java 成员。(不使用则默认无限制),可以选取下面的一个或者多个作为其值。

ElementType.ANNOTATION_TYPE 应用于注解类型。

ElementType.CONSTRUCTOR 应用于构造函数。

ElementType.FIELD 应用于字段或属性。

ElementType.LOCAL_VARIABLE 应用于局部变量。

ElementType.METHOD 应用于方法级注解。

ElementType.PACKAGE 应用于包声明。

ElementType.PARAMETER 应用于方法的参数。

ElementType.TYPE 应用于类的任何元素。

@Retention:这个注解最多保存到什么时候

RetentionPolicy.SOURCE 仅保留在源级别中,并被编译器忽略。

RetentionPolicy.CLASS 在编译时由编译器保留,但JVM会忽略。

RetentionPolicy.RUNTIME 由 JVM 保留。

从保留的时间长短上看: SOURCE < CLASS < RUNTIME

其他的有: @Documented:标记这些注解是否包含在用户文档(javadoc)中。 @Inherited:表示注解可被继承,父类使用的注解若带有@Inherited,子类会自动继承该注解。

从 Java 7 开始,额外添加了 3 个注解: @SafeVarargs:Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。 @FunctionalInterface:Java 8 开始支持,标识一个匿名函数或函数式接口。 @Repeatable:Java 8 开始支持,标识某注解可以在同一个声明上使用多次。

注解的使用

基本声明

一个注解的声明使用@interface关键字,通用的注解声明如下:

java 复制代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface AnnotationTest0{

}

@interface : Java中的所有注解,都默认实现 java.lang.annotation.Annotation接口。这是必须的。与implemented方法不同,使用@interface后不能继承其他注解或者接口。

注解类型元素

在上文元注解中,允许在使用注解时传递参数。我们也能让自定义注解的主体包含 annotation type element (注解类型元素) 声明,它们看起来很像方法,可以定义可选的默认值。

java 复制代码
@Target({ElementType.TYPE,ElementType.FIELD})
@Retention(RetentionPolicy.SOURCE)
public @interface AnnotationTest1{
	String value(); //无默认值
	int age() default 1; //有默认值
}

注解元素的类型只能是基本类型、String、Class、枚举、其他注解或这些类型的数组,且不能为null。

若注解仅有一个名为value()的元素,使用时可以省略元素名,直接传入值。

java 复制代码
@AnnotationTest1("帅") //如果只存在value元素需要传值的情况,则可以省略:元素名=
@AnnotationTest1(value="帅",age = 2)
int i;

注意:在使用注解时,如果定义的注解中的类型元素无默认值,则必须进行传值。

应用场景

按照 @Retention 定义的注解存储方式,可以分为三种使用场景

RetentionPolicy 存储位置 是否可反射获取 典型应用场景
SOURCE 仅源码 静态检查、代码生成
CLASS class文件 字节码增强、AOP
RUNTIME class文件+JVM内存 运行时动态处理

SOURCE

作用在源码上的注解,可以用作IDE语法检查,APT等场景。

取代枚举

Java中Enum(枚举)的实质是特殊单例的静态成员变量,在运行期所有枚举类作为单例,全部加载到内存中。比常量多5到10倍的内存占用。

例如: 我们定义方法 test ,此方法接收参数 teacher 需要在:LI、WANG中选择一个。如果使用枚举能够实现为:

java 复制代码
public enum Teacher{
	LI,WANG
}
public void test(Teacher teacher) {

}

而现在为了进行内存优化,我们现在不再使用枚举,则方法定义为:

java 复制代码
public static final int LI = 1;
public static final int WANG = 2;
public void test(int teacher) {
}

然而此时,调用 test 方法由于采用基本数据类型int,将无法进行类型限定。此时使用@IntDef增加自定义注解:

java 复制代码
public static final int LI = 1;
public static final int WANG = 2;
@IntDef(value = {LI, WANG}) //限定为LI,WANG
@Target(ElementType.PARAMETER) //作用于参数的注解
@Retention(RetentionPolicy.SOURCE) //源码级别注解
public @interface Teacher {
}
public void test(@Teacher int teacher) {
}

@IntDef和@StringDef属于Android支持库(androidx.annotation)中的特有注解,需添加依赖:implementation 'androidx.annotation:annotation:1.7.0'

此时,我们再去调用 test 方法,如果传递的参数不是 LI 或者 WANG 则会显示 Inspection 警告(编译不会报错)。 以上注解均为 SOURCE 级别,本身IDEA/AS 就是由Java开发的,Lint工具实现了对Java语法的检查,借助注解能对被注解的特定语法进行额外检查。

APT

APT全称为:"Anotation Processor Tools",意为注解处理器。顾名思义,其用于处理注解。编写好的Java源文件,需要经过 javac 的编译,翻译为虚拟机能够加载解析的字节码Class文件。注解处理器是 javac 自带的一个工具,用来在编译时期扫描处理注解信息。你可以为某些注解注册自己的注解处理器。 注册的注解处理器由 javac调起,并将注解信息传递给注解处理器进行处理。

注解处理器是对注解应用最为广泛的场景。在Glide、EventBus3、Butterknifer、Tinker、ARouter等等常用框架中都有注解处理器的身影。但是你可能会发现,这些框架中对注解的定义并不是SOURCE 级别,更多的是 CLASS 级别,别忘了:CLASS包含了SOURCE,RUNTIME包含SOURCE、CLASS。

CLASS

定义为 CLASS 的注解,会保留在class文件中,但是会被虚拟机忽略(即无法在运行期反射获取注解)。此时完全符合此种注解的应用场景为字节码操作。如:AspectJ、热修复Roubust中应用此场景。

所谓字节码操作即为,直接修改字节码Class文件以达到修改代码执行逻辑的目的。在程序中有多处需要进行是否登录的判断。

例如验证登录操作。如果我们使用普通的编程方式,需要在代码中进行 if-else 的判断,也许存在十个判断点,则需要在每个判断点加入此项判断。此时,我们可以借助AOP(面向切面)编程思想,将程序中所有功能点划分为: 需要登录 与 无需登录 两种类型,即两个切面。对于切面的区分即可采用注解。

java 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface Login {
}
@Login
public void jumpA(){
startActivity(new Intent(this,AActivity.class));
}
public void jumpB(){
startActivity(new Intent(this,BActivity.class));
}

在上述代码中, jumpA 方法需要具备登录身份。而 Login 注解的定义被设置为 CLASS 。因此我们能够在该类所编译的字节码中获得到方法注解 Login 。在操作字节码时,就能够根据方法是否具备该注解来修改class中该方法的内容加入 if-else 的代码段:

java 复制代码
@Login
public void jumpA() {
	if (this.isLogin) {
		this.startActivity(new Intent(this, LoginActivity.class));
	} else {
		this.startActivity(new Intent(this, AActivity.class));
	}
}
public void jumpB() {
	startActivity(new Intent(this,BActivity.class));
}

注解能够设置类型元素(参数),结合参数能实现更为丰富的场景,如:运行期权限判定等。

RUNTIME

注解保留至运行期,意味着我们能够在运行期间结合反射技术获取注解中的所有信息。

相关推荐
阿黄学技术7 分钟前
Spring单例Bean的线程安全
java·后端·spring
NMBG2219 分钟前
[JAVASE] Collection集合的遍历
android·java·开发语言·java-ee·intellij-idea
李长渊哦26 分钟前
Java中队列(Queue)和列表(List)的区别
java·list
爱敲代码的三毛44 分钟前
RabbitMQ可靠性进制
java·分布式·rabbitmq
云上艺旅1 小时前
K8S学习之基础三十一:k8s中RBAC 的核心概念
java·学习·云原生·kubernetes
追寻光2 小时前
Java 绘制图形验证码
java·前端
2301_792185882 小时前
maven的安装配置
java·maven
霸王龙的小胳膊2 小时前
SpringMVC-文件上传
java·mvc
哥谭居民00012 小时前
mybatis注册一个自定义拦截器,拦截器用于自动填充字段
java·开发语言·jvm·mybatis