Annotation

简介

Annotation(注解)是JDK5开始引入的新特性,可以看作是一种特殊的注释,主要用于修饰类,方法或者变量,在框架中大量使用(如 Spring、Mybatis等)

注解是一种能被添加到java代码中的元数据,类、方法、变量、参数和包都可以用注解来修饰。注解对于它所修饰的代码并没有直接的影响。

​ 下面是我简单写的一个自定义注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Test {
public int id() default -1;

public String msg() default "Hi";
}

​ 通过对上述文件的字节码(.class)的反编译(javap - p xxx.class)可以得到@interface其实就是一个继承了Annotation的一个接口

javap -p Test.class
public interface com.dyw.annotation.Test extends java.lang.annotation.Annotation {
public abstract int id();
public abstract java.lang.String msg();
}

​ 注解只有被解析之后才会生效,常见的解析方法有两种:

  • 编译期间直接扫描:编译器在编译Java代码的时候扫描对于的注解并处理,比如某个方法使用了@Override,编译器在编译的时候就会检测当前的方法是否重写了父类对于的方法。
  • 运行期间通过反射处理:这个经常在Spring框架中看到,例如Spring的@Value注解,就是通过反射来进行处理的。

注解详细介绍

我们通过上述的例子可以看到我们的注解上面还有着其他的注解例如@Retention@Target(这些都统称为元注解).

所以一个注解是由以下成分组成

  • 元注解
  • public @interface 注解名称

元注解介绍

​ JDK1.8版本为我们提供了6个标准的用来对注解类型进行注解的注解类(1.8之前只有四个),我们称之为meta-annotation(元注解).

元注解只能用在注解之上(自定义注解时可用)

image-20220407175816207

@Target

@Retention

@Documented

@Inherited

@Native(1.8新增)

@Repeatable(1.8新增)


@Target

官方解释:

指示注解类型适用的上下文。注解类型可能适用的声明上下文和类型上下文在 JLS 9.6.4.1 中指定,并在源代码中由java.lang.annotation.ElementType的枚举常量表示。
如果注解类型T上不存在@Target元注解,则类型T的注解可以写为除类型参数声明之外的任何声明的修饰符。
如果存在@Target元注解,编译器将强制执行ElementType枚举常量指示的使用限制,符合 JLS 9.7.4。
  • 它指明了它所修饰的注解使用的范围 如果自定义的注解为含有@Target元注解修饰,那么默认可以是在(除类型参数之外的)任何项之上使用,若有@Target元注解修饰那么根据Value(ElementType枚举常量)的指定的目标进行规定。
ElementType

ElementType.class

public enum ElementType {
/** 类、接口(包括注解类型)或枚举声明 */
TYPE,

/** 字段声明(包括枚举常量) */
FIELD,

/** 方法声明 */
METHOD,

/** 参数声明 */
PARAMETER,

/** 构造函数声明 */
CONSTRUCTOR,

/** 局部变量声明 */
LOCAL_VARIABLE,

/** 注解类型声明 */
ANNOTATION_TYPE,

/** 包装声明 */
PACKAGE,

/**
* 类型参数声明 类型参数即Map<String,Integer>中的String和Integer这里是作为类型
*
* @since 1.8
*/
TYPE_PARAMETER,

/**
* 使用类型 对应于 JLS 4.11 中的 15 个类型上下文,以及两个声明上下文:类型声明(包括注解类型声明)和类型参数声明。
*
* @since 1.8
*/
TYPE_USE
}

  • ElementType的枚举常量指明了注解可以使用的目标。
@Target(ElementType.METHOD)//可修饰在方法之上

@Retention

官方解释:

指示要保留带注解类型的注解多长时间。如果注释类型声明中不存在保留注释,则保留策略默认为RetentionPolicy.CLASS 。
仅当元注释类型直接用于注释时,保留元注释才有效。如果将元注释类型用作另一个注释类型中的成员类型,则它没有效果。
  • @Retention用来约束注解的生命周期,分别有三个值,源码级别(source)、类文件级别(class)或者运行时级别(runtime)可以通过指定@Retention中的值来实现(值为RetentionPolicy枚举常量)。
RetentionPolicy.class
public enum RetentionPolicy {
/**
* 注解将被编译器丢弃。(该类型的注解信息指挥保留在源码中,源码经过编译后,注解信息会被丢弃,不会保留在编译好的class文件中)
*/
SOURCE,

/**
* 注解将由编译器记录在类文件中,但不需要在运行时由 VM 保留。这是默认行为.(该类型的注解信息会保留在源码里和class文件里,在执行的时候,不会加载到虚拟机中) 该类型也是未指定@Retention值的缺省值
*/
CLASS,

/**
* 注解将由编译器记录在类文件中,并在运行时由 VM 保留,因此可以反射性地读取它们。(源码,class文件和执行时(VM)都保留注解的信息)
*/
RUNTIME
}

注意:生命周期大小排序为SOURCE < CLASS < RUNTIME,范围依次增大,前者能使用的地方后者一定能使用。如果需要在运行时去动态获取注解信息,那只能使用RUNTIME;如果要在编译时进行一些预处理操作,比如生成一些辅助代码,就是用CLASS;如果只是做一些检查性的操作,比如@Override和@SupperssWarning,可选择SOURCE


@Documented

官方解释:

表示默认情况下,带有类型的注释将由 javadoc 和类似工具记录。这种类型应该用于注解类型的声明,这些类型的注释会影响其客户对注释元素的使用。如果使用 Documented 对类型声明进行注释,则其注释将成为注释元素的公共 API 的一部分。
  • 带上该注解后的注解表明,在默认情况下这个注解是由JavaDoc和类似工具记录的,即带上了该文档化的注解被使用再生成文档时,会称为API的一部分。(默认情况下JavaDoc是不包含注解的,除非声明注解的时候使用了@Documented

Person.java

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Person {
String name() default "";
}

main.java

public class main {
@Person(name = "ding")
public static void main(String[] args) {

System.out.println("hello");
}
//@Person(name = "ding")
@Person(name = "yang")
public static void doSome(){

}
}
  • 生成的文档

image-20220407193958620

  • 不带@Documented注解生成的文档

image-20220407194037553


@Inherited

官方解释:

指示注解类型是自动继承的。如果注解类型声明中存在 Inherited 元注解,并且用户在类声明中查询注解类型,并且类声明没有该类型的注解,则将自动查询该类的超类以获取注解类型。将重复此过程,直到找到此类型的注释,或到达类层次结构(对象)的顶部。如果没有超类具有此类型的注释,则查询将指示所讨论的类没有此类注释。
请注意,如果注释类型用于注释类以外的任何内容,则此元注释类型无效。另请注意,此元注释仅导致注释从超类继承;已实现接口上的注解无效。
  • 被该元注解修饰的自定义注解再使用后会自动继承,如果使用了该自定义注解去修饰一个class那么这个注解也会作用于该class的子类。就是说如果某个类使用了被@Inherited修饰的注解,则其子类将会自动具有该注释

注意: @Inherited annotation类型是被标注过的class的子类所继承。类并不从它所实现的接口继承annotation,方法并不从它所重载的方法继承annotation

@Inherited //使用@Inherited修饰的自定义注解
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Person {
String name() default "";
}

main.java

public class main {
public static void main(String[] args) {
System.out.println(Father.class.getAnnotation(Person.class));
System.out.println(Son.class.getAnnotation(Person.class));
}

}

@Person
class Father {
}
class Son extends com.dyw.annotation.obj.Father {

}

image-20220407204033473


@Native

官方解释:

表示可以从本机代码引用定义常量值的字段。注释可以被生成本机头文件的工具用作提示,以确定是否需要头文件,如果需要,它应该包含哪些声明。
  • 使用 @Native 注解修饰成员变量,则表示这个变量可以被本地代码引用,常常被代码生成工具使用。对于 @Native 注解不常使用,了解即可。

@Repeatable

官方解释:

注释类型java.lang.annotation.Repeatable用于指示它(元)注释其声明的注释类型是可重复的。 @Repeatable的值表示可重复注解类型的包含注解类型。

@Repeatable允许在相同的程序元素中重复注解(不报错)。在需要对同一种注解多次使用时,往往需要借助 @Repeatable 注解。Java 8 版本以前,同一个程序元素前最多只能有一个相同类型的注解,如果需要在同一个元素前使用多个相同类型的注解,则必须使用注解“容器”。

不使用@Repeatable修饰的自定义注解完成重复注解

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

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Person {
String name() default "";
}
@Persons(value = {@Person(name = "ding"),@Person(name = "yang")})
public static void doSome(){

}

使用@Repeatable修饰的自定义注解

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

@Repeatable(Persons.class)
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Person {
String name() default "";
}
@Person(name = "ding")
@Person(name = "yang")
public static void doSome(){

}
  • 两种方法不同的地方是,创建重复注解Person时加上了@Repeatable注解,指向存储注解Persons,这样使用时就可以直接重复使用Person注解。从上述例子可以看出使用@Repeatable注解更符合常规思维,可读性强。

  • 但两种方法的效果相同,只是使用了@Repeatable注解简化了写法,这种简化的底层依旧是多个重复注解使用了一个被称作“容器”注解的value的成员的数组元素处理。


JDK基本注解介绍

​ 基本注解包括

  • @Override

  • @Deprecated

  • @SuppressWarnings

  • @SafeVarargs

  • @FunctionalInterface

@Override

​ 官方解释:

指示方法声明旨在覆盖超类型中的方法声明。如果使用此注解类型对方法进行注解,则编译器需要生成错误消息,除非至少满足以下条件之一:
该方法确实覆盖或实现了在超类型中声明的方法。
该方法的签名与Object中声明的任何公共方法的签名等效。
public class Father {
public String msg(){
return "";
}
}

public class Son extends Father{

@Override
public String msg() {
return "";
}
}
  • 如果将子类中方法名msg改为mg会发生如下编译错误

java: 方法不会覆盖或实现超类型的方法

  • 所以@Override的作用告诉编译器检查这个方法,保证父类要包含一个被该方法重写的方法,否者就会出错,这样可以帮助程序员避免一些低级错误。

@Deprecated

​ 官方解释:

@Deprecated 注释的程序元素是不鼓励程序员使用的程序元素,通常是因为它很危险,或者因为存在更好的替代方案。当在非弃用代码中使用或覆盖弃用的程序元素时,编译器会发出警告。
  • 通俗的说被该注解修饰的目标项是已经过时的了,不推荐使用的。**
public class Son extends Father{
@Deprecated
@Override
public String msg() {
return "";
}
}
  • 使用@Deprecated修饰了Son中的msg方法后,调用该方法会出现删除线和编译警告。

image-20220407210522333


@SuppressWarnings

​ 官方解释:

指示应在带注释的元素(以及带注释的元素中包含的所有程序元素)中抑制命名的编译器警告。请注意,给定元素中抑制的警告集是所有包含元素中抑制的警告的超集。例如,如果您注释一个类以抑制一个警告并注释一个方法以抑制另一个警告,则两个警告都将在方法中被抑制。
作为风格问题,程序员应该始终在最有效的嵌套元素上使用此注释。如果您想在特定方法中抑制警告,您应该注释该方法而不是它的类。
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
/**
*编译器要在带注​​释的元素中抑制的一组警告。允许重复名称。名称的第二次和连续出现将被忽略。存在无法识别的警告名称不是错误:编译器必须忽略它们无法识别的任何警告名称。但是,如果注释包含无法识别的警告名称,它们可以自由发出警告。
字符串"unchecked"用于抑制未经检查的警告。编译器供应商应结合此注释类型记录他们支持的其他警告名称。鼓励他们合作以确保相同的名称在多个编译器中工作。
回报:
要抑制的警告集
*/
String[] value();
}
  • Java中的@SuppressWarnings 注解指示被该注解修饰的程序元素(以及该程序元素中的所有子元素)取消显示指定的编译器警告,且会一直作用于该程序元素的所有子元素。

  • 如果你对于代码的规范不做要求又对编译器的警告感到烦躁那么你可以使用@SuppressWarnings(仅仅只是取消显示,并没有消除),它可以让你免去这些烦恼,当然编译器报错他是无法帮你取消显示的。

  • 添加前:

image-20220407211856114

  • 添加后:

image-20220407212004111

  • 注解的使用有以下三种:

    • 抑制单类型的警告:@SuppressWarnings("unchecked")
    • 抑制多类型的警告:@SuppressWarnings("unchecked","rawtypes")
    • 抑制所有类型的警告:@SuppressWarnings("unchecked")
  • 抑制警告的关键字如下表所示:

    关键字 用途
    all 抑制所有警告
    boxing 抑制装箱、拆箱操作时候的警告
    cast 抑制映射相关的警告
    dep-ann 抑制启用注释的警告
    deprecation 抑制过期方法警告
    fallthrough 抑制在 switch 中缺失 breaks 的警告
    finally 抑制 finally 模块没有返回的警告
    hiding 抑制相对于隐藏变量的局部变量的警告
    incomplete-switch 忽略不完整的 switch 语句
    nls 忽略非 nls 格式的字符
    null 忽略对 null 的操作
    rawtypes 使用 generics 时忽略没有指定相应的类型
    restriction 抑制禁止使用劝阻或禁止引用的警告
    serial 忽略在 serializable 类中没有声明 serialVersionUID 变量
    static-access 抑制不正确的静态访问方式警告
    synthetic-access 抑制子类没有按最优方法访问内部类的警告
    unchecked 抑制没有进行类型检查操作的警告
    unqualified-field-access 抑制没有权限访问的域的警告
    unused 抑制没被使用过的代码的警告

@SafeVarargs

​ 官方解释:

程序员断言带注释的方法或构造函数的主体不会对其 varargs 参数执行潜在的不安全操作。将此注释应用于方法或构造函数会抑制有关不可具体化的变量 arity (vararg) 类型的未经检查的警告,并抑制有关在调用站点创建参数化数组的未经检查的警告。
除了@Target元注解施加的使用限制外,编译器还需要对该注解类型实施额外的使用限制;如果使用@SafeVarargs注释对方法或构造函数声明进行注释,则这是编译时错误,并且:
声明是一个固定的arity方法或构造函数
声明是一个既不是static也不是final的变量 arity 方法。
鼓励编译器在将此注释类型应用于方法或构造函数声明时发出警告,其中:
变量 arity 参数具有可具体化的元素类型,包括原始类型、 Object和String 。 (对于可具体化的元素类型,此注释类型抑制的未经检查的警告已经不会出现。)
方法或构造函数声明的主体执行潜在的不安全操作,例如对变量 arity 参数数组的元素的赋值会生成未经检查的警告。一些不安全的操作不会触发未经检查的警告。例如,别名在
@SafeVarargs // 实际上并不安全!
static void m(List<String>... stringLists) {
Object[] array = stringLists;
List<Integer> tmpList = Arrays.asList(42);
array[0] = tmpList; // 语义上无效,但可以编译
String s = stringLists[0].get(0); // 哦不,运行时的 ClassCastException!
}

在运行时导致ClassCastException 。
该平台的未来版本可能会要求此类不安全操作出现编译器错误。

在学习@SafeVarargs之前先来看看下面有一段代码

public class main {
public static void main(String[] args) {
display("10",20,30);
}
public static <T> void display(T ...array){
for (T arg : array){
System.out.println(arg.getClass().getName()+":"+arg);
}
}
}

​ 这段代码中我设计了一个接收可变参数的方法public static <T> void display(T ...array){}可变参数方法中的参数类型相同,为此声明参数是需要指定泛型。

​ 但是调用可变参数方法时,应该提供相同类型的参数,但是代码中传入了不同类型的参数集合,此时可以看到display签名处有如下警告

image-20220407213806123

​ 翻译过来就是参数化可变参数类型可能造成的堆污染并且提示添加@SafeVarargs注解(仅仅起一个取消显示的作用,某种方面上来说是和 @SuppressWarnings 作用相同的)。

​ 这个警告是 unchecked(未检查不安全代码),就是因为将非泛型变量赋值给泛型变量所发生的。

image-20220407214116091

​ 可以发现加上了@SafeVarargs注解后编译器警告没有显示了,你肯定会说我使用@SuppressWarnings效果也是一样的,效果虽然一样,但是两者相较来说这里使用@SafeVarargs注解更合适

注意:@SafeVarargs注解不适用于非 static 或非 final 声明的方法,对于未声明为 staticfinal 的方法,如果要抑制 unchecked 警告,可以使用 @SuppressWarnings 注解。


@FunctionalInterface

​ 官方解释:

一种信息性注解类型,用于指示接口类型声明旨在成为 Java 语言规范定义的功能接口。从概念上讲,函数式接口只有一个抽象方法。由于默认方法有一个实现,它们不是抽象的。如果接口声明了一个覆盖java.lang.Object的公共方法之一的抽象方法,这也不会计入接口的抽象方法计数,因为接口的任何实现都将具有来自java.lang.Object或其他地方的实现(接口的实现是类,所有类的父类都是Object)。
请注意,函数式接口的实例可以使用 lambda 表达式、方法引用或构造函数引用来创建。
如果使用此注解类型对类型进行注解,则编译器需要生成错误消息,除非:
该类型是接口类型,而不是注解类型、枚举或类。
带注解的类型满足功能接口的要求。
但是,无论接口声明中是否存在FunctionalInterface注释,编译器都会将满足功能接口定义的任何接口视为功能接口。

​ 在学习Lambda表达式时,我们了解过函数式接口(接口中只有个一个抽象方法可以存在多个默认方法或多个static方法)。

  • @FunctionalInterface作用就是用来指定某一个接口必须是函数式接口的,所以@FunctionalInterface只能修饰接口。

  • 这里我写了两个抽象方法出现了编译器报错

image-20220407214859347

  • 这里我只写了一个抽象方法 一个static方法和一个默认方法 符合要求没有报错

image-20220407215038533

注意:如果接口声明了一个覆盖java.lang.Object的公共方法之一的抽象方法,这也不会计入接口的抽象方法计数

得出结论:@FunctionalInterface只是告诉编译器去检查这个接口是不是函数式接口,保证该接口只能包含一个抽象方法,否者就会出现编译错误。


写一个自己的注解

注解的格式就是

  • 元注解
  • public @interface 注解名
  • 注解内容体
@Documented //这里我希望我的注解能够生成在JavaDoc生成的文档中
@Retention(RetentionPolicy.CLASS) //这里我希望我的注解能够在字节码文件中保留
@Target({ElementType.TYPE,ElementType.METHOD}) //这里我希望我的注解可以修饰于类、接口、抽象类和方法上
public @interface MyAnnotation { //我声明了一个名为MyAnnotation的注解
//定义带两个成员变量的注解
//注解中的成员以方法的形式命名(注解的本质是接口) 并且可以带有默认值
String name() default "";
int age() default 0;
}

​ 想要获取注解中的成员变量需要使用反射的知识、首先需要获取类的Class对象,我们就可以通过的这个Class对象反射得到注解的成员变量了。

​ 反射相关的可以参考这篇博客反射

public class main {
public static void main(String[] args) throws NoSuchMethodException {
new Test().testMyAnnotation();
}

}
class Test{
@MyAnnotation(name = "dyw",age = 20)//这里我们使用了我们刚才自定义的注解
public void testMyAnnotation() throws NoSuchMethodException {
//这里获取Test类的Class对象 当类被编译后会当jvm加载时会生成该class文件的Class对象 通过这个Class对象可以完成反射相关的操作
Method testMyAnnotation = this.getClass().getMethod("testMyAnnotation");
//通过Class对象的getAnnotation()方法反射得到我们的MyAnnotation对象
MyAnnotation annotation = testMyAnnotation.getAnnotation(MyAnnotation.class);
//通过该对象可以获取注解类的成员
System.out.println("name :"+annotation.name());
System.out.println("age :"+annotation.age());
}
}

输出:

image-20220408151357416

​ 这里只介绍了自定义注解类的简单用法,但是却完美展现了注解搭配反射可以碰撞出的巨大火花。熟悉掌握注解与反射后就有一定能力去学习那些大佬开发的框架的底层代码了。


参考

Java注解 (biancheng.net)

java.lang.annotation (Java Platform SE 8 ) (oracle.com)

理解注解中的@Inherited - 掘金 (juejin.cn)

java元注解@Native && @Repeatable (java8 新增)_似火似水的博客-CSDN博客_native注解

反射

-END-