注解2
上一篇博客以Override注解简单的解释了一下注解的概念和用处,也稍微引申了一点元注解的概念。因为Override注解足够简单,所以这些概念看起来清晰明了;但是也因为其足够简单,所以和注解相关的很多内容Override并没有涉及到。这里我们自己定义一个注解:Myannot,来尽可能多的涵盖到注解的相关知识点。
Myannot基础结构
我们在工程中新建一个包:anno,并在这个包中定义Myannot,代码如下。可以看出这玩意基本就是照抄Override,区别只有两个:
- 修改Target元注解参数,使其只能标注于类或者接口上;
- 修改Retention元注解参数,使其生命周期从只能在源代码延展到可以存在于字节码和虚拟机中。
package anno;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Myannot {
}
Target和Retention两个元注解在上一篇博客中已经简单地描述过。修改两个元注解参数的原因:为了之后更方便地通过字节码观察不同注解元素的影响。比如现在我再对Person类打上Myannot注解,编译后就可以在Person的字节码中看到该注解,而不会像Override那样不直观(不犯个错你都不知道这玩意存不存在)。
代码语言:java复制import anno.Myannot;
@Myannot
public class Person {
public void hello() {
System.out.println("Person say hello");
}
}
其编译后字节码的反编译结果如下:
代码语言:java复制//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
import anno.Myannot;
@Myannot
public class Person {
public Person() {
}
public void hello() {
System.out.println("Person say hello");
}
}
注解参数
我们经常会看到,在给某个元素打上注解的时候,也会给注解配置一些参数。不说远的,前一篇博客里分析Override注解时,为什么其只能打在方法上?因为其元注解Target的参数设置为METHOD。一个注解有哪些参数?怎么给注解传递参数?
我们给Myannot注解定义两个参数:id和value,类型分别为int和String,默认值分别为1000和"Hello":
代码语言:java复制package anno;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Myannot {
int id() default 1000;
String value() default "Hello";
}
是的,尽管叫“参数”,但是其定义的形式看起来更像是函数。从这个定义中看,函数名就是参数名;函数返回值类型就是参数类型;default就是参数的默认值。
PS:接口:“这都是我的词啊!”
在注解中定义了参数,在之后的给元素打参数时,就可以对注解参数进行配置了。
代码语言:java复制import anno.Myannot;
@Myannot(id = 20, value = "HAHAHA")
public class Person {
public void hello() {
System.out.println("Person say hello");
}
}
观察到Person类的字节码反编译结果,可以看到对注解参数的更改。
代码语言:java复制//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
import anno.Myannot;
@Myannot(
id = 20,
value = "HAHAHA"
)
public class Person {
public Person() {
}
public void hello() {
System.out.println("Person say hello");
}
}
当然,虽然注解参数看起来就像是定义一个接口(interface),但他们多少还是有一定的差别的。
首先,你不能真的default一个函数在里面。比如你定义Sport接口的时候,default一个run函数,这在接口的定义中是可行的。但是放在注解的定义中,不允许,尽管run看起来完美符合注解参数的结构。
代码语言:java复制// 接口定义中,这样没有问题
public interface Sport {
default int run() {
return 0;
}
}
// 注解定义中,这样编译会报错
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Myannot {
int id() default 1000;
String value() default "Hello";
// 会报错
default int run() {
return 0;
}
}
其次,注解参数的数据类型只能为:基本类型、String、枚举类型、Class,以及它们的数组。说的直白点,你自己定义的类是不能作为注解参数的类型的。比如你在anno包中再定义一个Club,然后设置Myannot的一个参数返回类型为Club,会报错。
代码语言:java复制@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Myannot {
int id() default 1000;
String value() default "Hello";
// 报错 Invalid type 'Club' for annotation member
Club ppp();
}
到这里可能有的人会问(至少我一开始接触的时候就会问):然后呢?没了?这两个参数什么含义?有什么用?你设置了之后Person类怎么样了?对它有什么影响?目前看来,Myannot中这些参数没有任何用,对Person类没有任何影响。
请回顾一下上一篇博客中说的,注解到底是什么?注解的作用是标记,但也仅仅只是标记。把这些参数作为标记打在对应的元素上,注解的工作就已经完成了。这些参数有什么含义,会产生怎样的作用,这不是注解该操心的事情,而是处理该注解的程序该考虑的问题。至于怎么使用它们,将在后文“使用注解”中详细说。
总之记住:如果没有对应的注解处理程序去处理注解标记的内容,那注解就是个长得好看点的注释而已。
元注解
元注解的定义,以及Target和Rentention元注解在上一篇博客中已经解释过了,但是讲的很简单很肤浅,出于完整性考虑,还是把其基本内容简单地在这里描述一下,然后再来聊聊新的东西。
PS:我绝对不是在水字数。
打在注解定义头上的注解为元注解,也就是“注解的注解”。元注解用于给注解打上标记,让对应的程序知道该怎么处理这个注解本身。
我们可以先看看元注解的定义,这里以Target元注解为例:
代码语言:java复制package java.lang.annotation;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
ElementType[] value();
}
可以看出,元注解也是注解,所以元注解也可以被元注解标记。元注解的元注解叫什么?注解的注解的注解?元元注解?好吧,其实没那么无聊。元注解和普通注解并没有什么本质上的区别,只是打标记的目标不同而已。从上面的定义就可以看出,所谓元注解,只是其Target元注解的参数为ANNOTATION_TYPE,表示这玩意只能打在注解上,和Override这种指定了Target参数而只能打在函数上的普通注解没什么不一样。你要是开心的话你也可以自己定义元注解。
官方给出的元注解并不多,就下面几个,均定义于java.lang.annotation包中。
Target
Target元注解定义了注解允许被标注在哪些类型的声明上。其参数类型为ElementType枚举类型,其常用枚举值如下,更多枚举值请直接查看其定义。
代码语言:java复制TYPE // 类或者接口
FIELD // 属性
METHOD // 方法
PARAMETER // 方法参数
CONSTRUCTOR // 构造方法
Retention
Retention元注解决定了这个注解的生命周期,定义为:
代码语言:java复制package java.lang.annotation;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
RetentionPolicy value();
}
其参数类型为RetentionPolicy,有如下三个枚举值:
代码语言:java复制SOURCE // 源代码期
CLASS // 字节码期
RUNTIME // 运行期
这次我们自己定义的Myannot可以在字节码中被观察到就是因为我们将该注解的参数改为了“RUNTIME”,使得Myannot在字节码中及虚拟机中均可以存在。
Inherited
这是个新东西,我觉得从这个单词的意思也能大概猜出来这玩意是干什么的:它决定了打在父类身上的标记,啊不对,是注解,会不会也被打在其派生出的子类上。其定义如下:
代码语言:java复制package java.lang.annotation;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {
}
还记得我们在这篇博客的一开始,将Myannot注解打在了Person类上并生效,那Person类的子类Student类,有没有也被打上同样的标签?我们可以看看Student反编译出来的字节码:
代码语言:java复制//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
public class Student extends Person {
public Student() {
}
public void hello() {
System.out.println("Student say hello");
}
}
可以看出,作为Person的子类,打在Person类上的注解并没有被打在Student类上。但是,如果给Myannot注解打上Inherited元注解,那情况就不一样了。我们给Myannot注解打上Inherited元注解,其他的都不改,并重新编译。
代码语言:java复制package anno;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited // 给Myannot注解打上Inherited元注解
public @interface Myannot {
int id() default 1000;
String value() default "Hello";
}
那再猜猜,此时Student类的字节码有没有也被打上Myannot注解?这次恐怕大多数人会猜错:
代码语言:java复制//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
public class Student extends Person {
public Student() {
}
public void hello() {
System.out.println("Student say hello");
}
}
从字节码上看,Student上还是没有Myannot注解。这是什么情况?Inherited元注解失效了?还是IDEA用的编译器坏了?其实都没有,它确实实现了注解的继承,只是字节码中并没有显示出来。那口说无凭,怎么证明Myannot注解也确实打在了Student类上呢?这个放在后文“使用注解”的部分再详细说。
Repeatable
同样可以通过单词的意思判断出这个注解的作用:表示这个注解能不能被重复标记。如果没有这个元注解,同一个注解在同一个元素上只能打一次,否则会报错,不信可以自己试一下。
Repeatable注解的定义如下:
代码语言:java复制package java.lang.annotation;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Repeatable {
Class<? extends Annotation> value();
}
该元注解有一个类型为Class的参数,这个参数代表什么意思?我们先举个栗子。
首先,我们要再创建一个注解Myannots,这个注解有一个参数value,类型为Myannot数组。
代码语言:java复制package anno;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Myannots {
Myannot[] value();
}
然后我们再对Myannot注解打上Repeatable注解,参数设置为我们刚刚定义的注解Myannots的Class。
代码语言:java复制package anno;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Myannots.class)
public @interface Myannot {
int id() default 1000;
String value() default "Hello";
}
然后我们就可以在Person类上再打一个Myannot注解了:
代码语言:java复制import anno.Myannot;
@Myannot(id = 20, value = "HAHAHA")
@Myannot(id = 30, value = "LALALA")
public class Person {
public void hello() {
System.out.println("Person say hello");
}
}
为什么要这么麻烦?我们可以看看Person在打上两个Myannot注解后,反编译出来的内容是什么:
代码语言:java复制//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
import anno.Myannot;
import anno.Myannots;
@Myannots({@Myannot(
id = 20,
value = "HAHAHA"
), @Myannot(
id = 30,
value = "LALALA"
)})
public class Person {
public Person() {
}
public void hello() {
System.out.println("Person say hello");
}
}
从源代码的角度上看,好像Myannot注解在Person上打了两次;从字节码的角度,其实还是同一个注解在同一个元素上只能打一次。只是这次打的注解是我们后来定义的参数为数组的Myannots。这也就是为什么在刚才的操作中我们必须再额外创建一个注解数组(这么说不太准确,但是能理解就行),然后把它的Class作为参数传递给Repeatable注解。
Document
我们在前文的很多元注解的定义中可以看到这个元注解,简而言之就是用这个元注解标记的注解会在生成的java doc中出现,这里就不举例了,只给出该元注解的定义。
代码语言:java复制package java.lang.annotation;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {
}
至此,注解的定义所必要的内容我就讲完了(大概吧),下一篇博客会写注解的使用。毕竟注解只是一个标记,如果没有对应的处理,那注解不过是个长得好看的注释而已。