💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、豆包、星火、月之暗面及文生图、文生视频 广告
[TOC] # 注解(Annotation) ## 本文目的 如果你是 Java 新手,之前从来没有阅读过关于注解的资料及文章,那么我希望通过本篇文章,可以让你了解注解的基本概念,读懂应用了注解的代码。 ## 概念 我们先来看下面这个例子 ~~~ // Chinese 类作为 Person 类的子类,重写 speak() 方法 public class Person { public void speak() { System.out.println("I can speak"); } } public class Chinese extends Person { public void speak1() { //不小心把 speak 误写成 speak1 了 super.speak(); System.out.println("I also can speak Chinese"); } } ~~~ 重写方法时写错了方法名,这种情况编译器不会有任何警告,因为它会认为新增了一个方法。假如现在调用 Chinese.speak(),执行的还是父类的方法,显然不是需要的结果,当然你可以说:“我永远也不会犯这种低级错误”,好吧,不过我还是要向你推荐注解,因为它可以帮我们避免这种错误,而且可以增加代码的可读性。 ~~~ public class Chinese extends Person { //1.添加 @Override 注解 //2.此时编译器报错Method does not override method from its superclass //3.发现错误,将 speak1 改为 speak ,警告消失,编译通过 @Override public void speak1() { super.speak(); System.out.println("I also can speak Chinese"); } } ~~~ 你可以把注解当作一个特殊的修饰符(类似 final , static)。 **@Override** 非常直观地表明这个方法是一个重写的方法,并且当我们误写的时候,编译器就会发出警告。它是 Java sdk 中自带的注解,自带的常用的注解还有 `@SuppressWarnings` , `@Deprecated` 等,可以通过搜索,了解具体的作用。 ## 语法 ~~~ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.SOURCE) public @interface Override { } ~~~ 这是 `@Override` 类的代码,我们就根据它的代码来学习注解的语法。 首先是声明一个注解:`public @interface Override{}`,和定义一个接口很像,唯一不同的是需要加一个 @ ,再引入一个概念:**元注解**,元注解是修饰注解的注解,下文有详细介绍。 这两行代码就是使用了 `@Target` 和 `@Retention` 2个元注解修饰了 `@Override` 注解,就好像刚刚我们在方法的声明上写了一个 `@Override` ,这里的元注解也是写在注解的声明上的。另外我们得知,所谓的“元注解修饰注解”是发生在注解类中的,而不是发生在使用注解的时候 ~~~ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.SOURCE) ------------------------------------------------------------------------------------- //错误的示范 public class Person { @Target({ElementType.METHOD}) @Retention(RetentionPolicy.SOURCE) @Override public String toString(){ //重写 toString 方法 } } ~~~ 再来看一下 **@Target** 的代码 ~~~ @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.ANNOTATION_TYPE}) public @interface Target { ElementType[] value(); } ~~~ 重点来看这行代码 `ElementType[] value();`,转换成通用格式应该是 `参数类型 参数名称() default 默认值;`,`default 默认值` 是可选的,因为这里的参数类型是 `ElementType[]` ,所以 `@Target({})` 使用了花括号,并且可以传入多个值,形成数组。还有一点需要注意:注解定义了参数之后,参数所传入的值的类型并非所有类型都可以,它只能是下列这些类型: * 所有基本数据类型(int,float,boolean,byte,double,char,long,short) * String * Class * enum * Annotation * 以上所有类型的数组 这里的 ElementType[] 就是一个枚举类的数组 `@Target({ElementType.ANNOTATION_TYPE})` 是 `@Target( value = {ElementType.ANNOTATION_TYPE})` 的省略形式,因为注解的规则是:没有定义任何参数的注解称为**标记注解**,就像刚才的 `@Override`,如果定义了一个参数,那么最好将参数名称定义为 value,这样在传入参数的时候,可以省略 `value=` 直接写入值,当然你不定义为 value 的话,也是可以的,只不过无法使用这个省略形式。例如: ~~~ //定义一个注解,参数的值的类型设定为 String public @interface MyAnnotation { String value() ; } //没有任何问题 @MyAnnotation("HelloWorld!") public class Person{ ... } //假如我们不将名称写为 value public @interface MyAnnotation{ String query(); } //必须要写成 参数名称 = 值 的形式 @MyAnnotation(query = "HelloWorld") public class Person{ ... } //default 的示范 public @interface MyAnnotation { String value() default "HelloWorld" ; } //不传入值的时候等价于 @MyAnnotation("HelloWorld") //也可以传入值覆盖掉 default 值, @MyAnnotation("WorldHello") @MyAnnotation public class Person{ ... } ~~~ 所以注解在使用时的通用格式应该是: * 定义了参数的注解: @注解名(参数名称 = 要传入的值) * 标记注解或者定义了 default 值的注解: @注解名 ## 元注解 目前有 5 个元注解,分别是: * Target * Retention * Documented * Inhertied * Repeatable **@Target** 决定了你的注解可以修饰哪些对象,如果你希望自己的注解只用来修饰方法,那么用你的注解修饰成员变量的话,就会出错。我们的注解可以修饰下列这些对象,是 `java.lang.annotation` 包下的一个枚举类 `ElementType` 提供的值 : * TYPE :用于描述类,接口,注解类,枚举类 * FIELD :全局变量(定义在类中的变量) * METHOD :方法 * PARAMETER :方法中的参数 * CONSTRUCTOR :构造函数 * LOCAL_VARIABLE :成员变量(定义在方法中的变量) * ANNOTATION_TYPE :希望这个注解只能修饰注解类(注意和 TYPE 区分) * PACKAGE * TYPE_USE * TYPE_PARAMETER 有 3 个没有做解释的,我们分别说一下: **PACKAGE** :从字面上看的话,我们会联想到它的使用场景,应该是这样的: ~~~ @MyAnnotation package com.example; public class Person{ ... } ~~~ 这样做的话,我们会得到一个错误 `Package annotations should be in file package-info.java`,为什么呢?因为对于包的注释不能放在类中,而是要在这个包的目录下创建一个 package-info.java 来写注释,如果你的 Java 基础够好,你可能可以看出来," - " 这个符号是不能出现在类的命名中的(我就没看出来,╥﹏╥...),所以不能直接创建一个类,做法是在包下新建一个文件,然后将它改名为这个名字,在 Android Studio 中的操作是:右键点击一个包 -> New -> File -> 输入 package-info.java 就可以了,接下来我们只需要在 `package-info.java` 中添加这两行代码。 ~~~ @MyAnnotation package com.example; //包名,如果 MyAnnotation 是别的包中的,记得 import ~~~ **TYPE_PARAMETER** : Java 8 新增的,与**类型,参数**有关,但是它无法修饰方法中的参数,它可以修饰泛型中的类型参数,像这样: ~~~ public class Test<@MyAnnotation T> { ... } ~~~ **TYPE_USE** :Java 8 新增的,可以用于任何涉及到**类型**的情况,如果选取这个值,相当于你直接实现了这些值所实现的功能,不仅如此,还可以修饰其他的一些对象,可以参考[Type](https://dzone.com/articles/java-8-type-annotations) * TYPE * FIELD * PARAMETER * CONSTRUCTOR * LOCAL_VARIABLE * TYPE_PARAMETER 不过它仍然无法修饰方法和包。 **@Retention** 决定了注解的生命周期,`java.lang.annotation` 包下有一个枚举类 `RetentionPolicy` , 它提供了注解可选的生命周期,不同的生命周期往往决定了注解何时发挥它的作用(如果该注解我们只希望它在编译的时候产生作用,那我们就不需要把它保留到程序运行的时候)。 * SOURCE :只在 .java 文件中保留 * CLASS : 将 .java 文件编译成 .class 文件后,还会保留在 .class 文件中 * RUNTIME :程序运行时,注解仍然有效 **@Documented** 决定了是否需要在生成的 Api 说明中体现该注解。例如,我们现在用一个注解修饰了一个方法 ~~~ @MyAnnotation //这里的 @MyAnnotation 使用了 @Documented 修饰 public void speak(){ } ~~~ 下面是使用 JavaDoc 生成d 2 份文档,体现了 @Documented 的作用: ![无 Documented](http://or4ad70xa.bkt.clouddn.com/%E6%97%A0Documented.png) ![有 Documented](http://or4ad70xa.bkt.clouddn.com/%E6%9C%89Documented.png) **@Inherited** 决定了注解是否可以被继承,这里的继承不是针对注解与注解之间的继承,仍然是针对 Java 类中的继承,具体的作用在后面的“简单应用”中说明。 **@Repeatable**是 Java 8 新增的,具体的作用在后面的“简单应用”中说明。 ## 简单应用 ~~~ //我们自定义了一个 Annotation @Retention(RetentionPolicy.RUNTIME)//这里定义为 RUNTIME ,可以配合反射使用 public @interface MyAnnotation { String value(); } //抽象类 InheritedTest 使用了 @MyAnnotation @MyAnnotation("ClassAnnotation") public abstract class InheritedTest { @MyAnnotation("NormalMethod") public void normalMethod() { } @MyAnnotation("ImplementsMethod") public abstract void implementsMethod(); @MyAnnotation("OverrideMethod") public void overrideMethod() { } } ~~~ 我们的问题是:如果现在有一个类继承了 `InheritedTest` 类,那么在 InheritedTest 中定义的注解会不会都被继承?`MyAnnotation` 有没有用 `@Inherited` 修饰会不会影响结果?我们先来证明这个问题。 ~~~ //定义一个子类 public class SubClass extends InheritedTest { @Override public void implementsMethod() { } @Override public void overrideMethod() { } } //main 方法 public class Test { public static void main(String[] args) throws Exception { Class clazz = SubClass.class; Method methodAbstract = clazz.getMethod("implementsMethod"); Method methodOverride = clazz.getMethod("overrideMethod"); Method methodNormal = clazz.getMethod("normalMethod"); //检查修饰类的注解是否被继承 if (clazz.isAnnotationPresent(MyAnnotation.class)) { MyAnnotation myAnnotation = (MyAnnotation) clazz.getAnnotation(MyAnnotation.class); System.out.println("子类继承到父类类上Annotation,其信息如下:" + myAnnotation.value()); } else { System.out.println("子类没有继承到父类的 Annotation"); } //检查实现抽象的方法的注解是否被继承 checkAnnotation(methodAbstract); //检查重写了父类方法,注解是否可以被继承 checkAnnotation(methodOverride); //检查子类直接继承的方法,注解是否可以被继承 checkAnnotation(methodNormal); } public static void checkAnnotation(Method method) { if (method.isAnnotationPresent(MyAnnotation.class)) { MyAnnotation myAnnotation = method.getAnnotation(MyAnnotation.class); System.out.println(method.getName() + ":继承到父类方法上的Annotation,其信息如下" + myAnnotation.value()); } else { System.out.println(method.getName() + "没有继承到父类方法上的Annotation"); } } } result : 子类没有继承到父类的 Annotation implementsMethod没有继承到父类方法上的Annotation overrideMethod没有继承到父类方法上的Annotation normalMethod:继承到父类方法上的Annotation,其信息如下NormalMethod (当 MyAnnotation 被 @Inherited 修饰时的结果) result: 子类继承到父类类上Annotation,其信息如下:ClassAnnotation implementsMethod没有继承到父类方法上的Annotation overrideMethod没有继承到父类方法上的Annotation normalMethod:继承到父类方法上的Annotation,其信息如下NormalMethod ~~~ 通过上述的结果,我们可以得出这样的结论:`@Inherited` 只会影响修饰类的注解的继承,关于方法的注解,即使不通过 `@Inherited`,子类继承了方法后,也可以继承到注解,但如果是通过“实现”或者“重写”的方法,那么就无法继承到注解。 下面这个例子是关于 **使用@Repeatable** ~~~ //我们现在想要使用这样一个效果:让 Test 类被 MyAnnotation 修饰 2 次 @Retention(RetentionPolicy.RUNTIME) public @interface MyAnnotation { String value(); } @MyAnnotation("Hello") @MyAnnotation("World") public class Test { ... } ~~~ 这样做是行不通的,同样的注解不能多次修饰同一个对象,但是如果一定要要求这样做,那么可以这样: ~~~ //再定义一个注解,将参数接收的值的类型设置为 MyAnnotation[] ,即可以接收多个 MyAnnotation @Retention(RetentionPolicy.RUNTIME) public @interface AnnotationArray { MyAnnotation[] value(); } //main 方法 @AnnotationArray({ @MyAnnotation("Hello"), @MyAnnotation("World") }) public class Test { public static void main(String[] args) { Class clazz = Test.class; AnnotationArray annotationArray = (AnnotationArray) clazz.getDeclaredAnnotation(AnnotationArray.class); for (MyAnnotation myAnnotation : annotationArray.value()) { System.out.println(myAnnotation.value()); } } } result : Hello World ~~~ 我们完成了这个效果,不过在使用 `@AnnotationArray` 的时候显得过于复杂,`@Repeatable` 可以改善这一情况。 ~~~ @Repeatable(AnnotationArray.class) //使用 @Repeatable @Retention(RetentionPolicy.RUNTIME) public @interface MyAnnotation { String value(); } @AnnotationArray 的代码不变 @MyAnnotation("Hello") //现在可以使用这种多次注解的形式了 @MyAnnotation("World") public class Test { public static void main(String[] args) { Class clazz = Test.class; MyAnnotation[] myAnnotations = (MyAnnotation[]) clazz.getDeclaredAnnotationsByType(MyAnnotation.class); for (MyAnnotation myAnnotation : myAnnotations) { System.out.println(myAnnotation.value()); } } } result : Hello World ~~~ **@Repeatable** 在使用时需要传入一个 Class 的值,这个 Class 是一个注解类的 Class,而且这个注解的参数接受的值的类型应该是一个数组,数组的类型就是我们需要多次使用的注解,所以在这个例子中我们传入 `AnnotationArray.class` 。 ## 参考 [ElementType.PACKAGE的使用](http://www.cnblogs.com/ycoe/archive/2009/09/26/1574565.html) [@Inherited的测试](http://elf8848.iteye.com/blog/1621392) [Repeatable 的使用](http://www.jianshu.com/p/28edf5352b63) [Java注解全面解析](http://9leg.com/java/2016/01/21/java-annotation.html) [一小时搞明白自定义注解](http://blog.csdn.net/u013045971/article/details/53433874)