注解又称 Java 标注,是 JDK 5.0 引入的一种注释机制。通俗点来说就是在某个地方做个标记,方便对其进行扩展。注解在编译之后会成为 class 文件,且是接口类,但它不能被继承。
下面简单介绍一下它的语法,Java 是支持自定义注解的,示例如下:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @interface A { String value(); }
只需要使用 @interface 即可声明一个注解,可用修饰符与类声明时一致,唯一不同的是,注解实际上是没有方法,它只有属性,但是它的属性表面上又不是属性,它表现出来的是方法,比方说上面的
String value();
你以为它是声明了一个方法,实际上人家是声明了一个变量,按照类的写法,它等同于
String value;
同理,加入我想在注解 A 中再添加几个属性,包括字符串数组类型的,则可以这样写:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @interface A { String value(); String[] names(); int id(); }
这相当于又添加了两个属性:
String[] names; int id;
需要特别注意,属性的类型只支持 基本数据类型 + String + Class + 枚举类型 + 注解类型 以及由它们组成的数组 !
同时在声明注解时可以给指定的属性赋予默认值,使用 default 来给它们赋予默认值:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @interface A { String value(); String[] names() default {}; int id() default 100; }
有些小伙伴可能就要问了,这个 default 设置默认值有什么用吗?要知道存在及合理,这是因为对于自己所声明的注解的每个属性在使用时都必须有值存在,这里可不会像类加载时一样给你初始化一个默认值出来,你必须使用 default 来设置默认值,它才会自动初始化为你所指定的值,否则便属于无值。如果没有设置,那么在使用的时候就必须显式给它赋值,但是假如这个值每次显式赋值都是一样的值,岂不是要做许多重复工作吗?但是又不得不给它赋值,因为属性无值会编译出错,那么这个时候就必须使用 default 来给这个属性指定默认值了,这样就不必做这许多无用功了,也就是可以不用显式给它赋值!
下面声明了一个注解 A ,可以标注在类上(由@Target指定),注解在运行时使用(由@Retention指定):
package cn.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @interface A { String value(); String[] names() default {}; int id(); } // 注解这样使用,value 和 id 是定义的属性,以键值对形式赋值 @A(value = "value", id = 100) public class TestAnnotation { public static void main(String[] args) throws Exception { Class<?> main = Class.forName("cn.annotation.TestAnnotation"); A a = main.getAnnotation(A.class); System.out.println(a.value()); } }
这里的 id 属性没有赋予默认值,那么在使用的时候就必须显式给它赋值 id = 100,但给它设置默认值之后,就像这样:
package cn.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @interface A { String value(); String[] names() default {}; int id() default 100; } // 注解这样使用 @A("value") public class TestAnnotation {}
这里显然不用给 id 显式赋值,但有小伙伴又好奇了,value 属性也没有设置默认值啊,这里怎么没有 value = "value" 的键值对,而只有 "value" 这一个值呢?这就是注解的一个默认实现了,但只有在仅有属性名为 value 的属性需要赋值时,可以不必显式写成 value = "value" 的形式,而只需要简写成 "value" 值的形式即可(只要有两个及以上的属性需要被赋值就不能这样简写),特别注意,只有属性名为 value 的属性可以这样操作,比方说下面这个例子:
package cn.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @interface A { String name(); String[] names() default {}; int id() default 100; } @A(name = "value") public class TestAnnotation {}
此时,也只有一个 name 属性没有默认值,需要被显式赋值,但是它却不可以像名为 value 的属性一样简写成 "value" ,而必须使用键值对形式 !
在简单地看了注解的定义和使用后,或许有些小伙伴会有疑问,注解 A 上的注解 @Target 和 @Retention 是什么意思,显然在我们会使用注解后,可以发现它们也是注解,更具体地来说,它们是 Java 最基础的注解,主要包括四个:
@Documented – 注解是否将包含在JavaDoc中 @Retention – 什么时候使用该注解 @Target – 注解用于什么地方 @Inherited – 是否允许子类继承该注解
小板凳放好,一个一个来。
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Retention { // 保留策略: 也就是说这个注解信息在什么时候有效 RetentionPolicy value(); }
RetentionPolicy 是一个枚举类型,其内有三个值:
- SOURCE : 源码阶段,表示只在编译前即编辑源代码时生效,编译时信息会被丢弃,不会被编译进字节码文件中 - CLASS : 字节码文件阶段,表示在编译时信息会被编译器写入字节码文件中,但是当虚拟机运行时信息不会被保留,当没有指定任何值时,会默认采用这种方式保存信息 - RUNTIME : 运行时阶段,表示信息会被编译器写入字节码文件中,而且在虚拟机运行时这些信息也还会保留,因此只有这种方式保存的信息才可以被反射所获取
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Target { // 元素类型数组: 说明这个注解可以标注在什么地方,它是一个数组,说明一个注解可以标注在多个不同的地方 ElementType[] value(); }
ElementType 也是一个枚举类型,就是说注解可以标注的地方,主要有以下几个值:
- TYPE : 用于类, 接口(包括注解接口),或者枚举类型声明 - FIELD : 用于成员变量声明(包括枚举类型常量声明) - METHOD : 用于方法声明 - PARAMETER : 用于参数声明 - CONSTRUCTOR : 用于构造器声明 - ANNOTATION_TYPE : 用于注解类型声明 - LOCAL_VARIABLE : 用于局部变量声明 // 保留策略只能是源码阶段 - PACKAGE : 用于包声明 // 保留策略也只能是源码阶段 - TYPE_PARAMETER : 类型参数声明 - TYPE_USE : 用于类型使用 - MODULE : 模块声明
当你需要你的注解可以注解在类声明或者方法声明上时你可以这样设置(需要注解在什么地方就加哪个地方):
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @interface A { String value(); String[] names() default {}; int id() default 100; }
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Inherited { }
可以看到它什么属性也没有,就是一个简单标记类注解,那有什么用?答案就是它所标注的注解类可以被继承!什么意思?前面不是说注解虽然是接口,但它不能被继承,这不是自相矛盾吗?所以啊,肯定不是类与类之间的那种继承,但也和它们有关,所谓注解被继承,是说假如有一个可被继承的注解标注在一个父类上,那么它的所有子类都会拥有这个可被继承的注解而不用显式注解到它的子类上。这里存个疑,后续将编写代码对其进行测试。
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Repeatable { // 用于保存多个注解的容器 Class<? extends Annotation> value(); }
该注解表明标记的注解可以多次应用于相同的声明或类型,此注解由Java SE 8版本引入。在此之前,一个同类型的注解在一个位置,例如类声明上,只能出现一次,而有了这个注解后,被这个注解所注解的注解类将被允许在同一位置出现多次,尽管它们是相同的注解类型。下面的示例将演示在没有 @Repetable 这个注解前(即原始方法)如果要在同一个位置多次声明一个同类型的注解要怎么办?
package cn.annotation; import java.lang.annotation.*; @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @interface B { String value() default ""; } @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @interface BS { // 注解数组 B[] bs(); } @BS(bs = { @B("1"), @B("2"), @B("3") }) public class TestAnnotation {}
没错,就这样子,声明另一个注解类保存多个同类型的注解信息。那有了 @Repetable 这个注解后(新技术)怎么做?请开始我的表演:
package cn.annotation; import java.lang.annotation.*; @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Repeatable(BS.class) @interface B { String value(); } @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @interface BS { // 注解数组 B[] value(); } @B("1") @B("2") @B("3") public class TestAnnotation { public static void main(String[] args) throws Exception { Class<?> main = Class.forName("cn.annotation.TestAnnotation"); // 获取所有声明的注解,包括从父类继承来的注解 for (Annotation annotation : main.getAnnotations()) { System.out.println(annotation); } System.out.println("-------\n"); } }
看起来简洁得多了,细心的小伙伴们或许发现了其中的几处变化,首先 @Repetable 注解添加在了需要重复声明的注解 B 上,而且指定其值为 BS 注解,前面有说过这个 @Repetable 的值是作为容器存在的,谁的容器?是那几个被重复声明的注解 B 的!其次,BS 中的属性名由 bs 变成了 value,是的,不是 value 还不行,编译不通过。
其实,只要我们稍微对比一下这两种方法(原始方法和新技术之对比),就可以发现新技术其实就是原始方法,只是在使用方式上更简洁友好了。据推测,同一类型注解不能重复注解在同一位置上的原则没有变,变化的是在这之上的变化,我们可以在接下来的反射例子中感受到这一点。
由于注解的保留策略中允许将注解信息写入字节码文件中,同时又可以在虚拟机运行时保留这些信息,所以当虚拟机将字节码文件实例化为 Class 对象时,就可以获取到该类所对应的注解信息!下面我们来简单演示一下:
package cn.annotation; import java.lang.annotation.*; @Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.LOCAL_VARIABLE}) @Retention(RetentionPolicy.RUNTIME) @interface A { String value(); String[] names() default {}; int id() default 100; } @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @interface B { String value() default ""; } @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Inherited @interface C {} @B @C class Super {} @A("类注解") public class TestAnnotation extends Super { @A("成员变量注解") private int i; @A("构造器注解") public TestAnnotation() { } @A("方法注解") private void method(@A("参数注解") int param){ @A("局部变量注解") int j = 0; } public static void main(String[] args) throws Exception { Class<?> main = Class.forName("cn.annotation.TestAnnotation"); // 获取所有声明的注解,包括从父类继承来的注解 for (Annotation annotation : main.getAnnotations()) { System.out.println(annotation); } System.out.println("-------\n"); // 获取所有只在本类上声明的注解信息,不包括从父类继承来的注解 for (Annotation annotation : main.getDeclaredAnnotations()) { System.out.println(annotation); } System.out.println("-------\n"); // 如果此类有指定类型(A类型)注解信息 if (main.isAnnotationPresent(A.class)) { // 那么根据该指定类型的注解查找注解信息 System.out.println(main.getAnnotation(A.class)); System.out.println(main.getDeclaredAnnotation(A.class)); } else { System.out.println("此类不存在该指定注解信息"); } } } 运行结果: @cn.annotation.C() @cn.annotation.A(names=[], id=100, value=类注解) ------- @cn.annotation.A(names=[], id=100, value=类注解) ------- @cn.annotation.A(names=[], id=100, value=类注解) @cn.annotation.A(names=[], id=100, value=类注解)
举一反三,获取类对象上的注解需要类对象,那么获取构造方法、成员变量以及成员方法,自然就是获取对应的构造器、领域器和方法器,然后再调用注解相关方法,名字都是一样的或者见字如面!
上面的例子里面也还演示了 @Inherited 的作用,父类被注解 B、C 修饰,而子类只有 A 修饰,但是获取子类上所有的注解信息时注解 C 的信息也出现了,这就是注解的继承性,另外,如果子类重新声明了一个父类已声明的可被继承的注解,那么子类将覆盖父类的。
不知道各位还记不记得 @Repetable 这个注解,说这个新技术和它的原始方法其实是一样的,只是新技术更简洁又好了,下面给出佐证:
package cn.annotation; import java.lang.annotation.*; @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Repeatable(BS.class) @interface B { String value(); } @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @interface BS { // 注解数组 B[] value(); } @B("1") @B("2") @B("3") public class TestAnnotation { public static void main(String[] args) throws Exception { Class<?> main = Class.forName("cn.annotation.TestAnnotation"); Annotation[] annotations = main.getAnnotations(); System.out.println("本类上声明的注解数量: " + annotations.length); if (main.isAnnotationPresent(BS.class)) { BS bs = (BS) annotations[0]; for (B b : bs.value()) { System.out.println(b); } } else{ System.out.println("本类上没有声明 BS 注解!"); } System.out.println("那么有没有声明 B 注解? " + main.isAnnotationPresent(B.class)); } } 运行结果: 本类上声明的注解数量: 1 @cn.annotation.B(value=1) @cn.annotation.B(value=2) @cn.annotation.B(value=3) 那么有没有声明 B 注解? false
运行结果中并没有打印出“本类上没有声明 BS 注解!”这句话,而且明确说明了没有声明过 B 注解。那是这样的吗?回去检查一下代码,你会发现这根本就是在胡说八道嘛,我不仅没有声明过 BS 注解,我还声明了 3 个 B 注解,这难道是河里吗,这不合理,但事实胜于雄辩。因此,我们可以初步断定,它们最终的实现原理是一样的。
最后,以 getAnnotationsByType 这个方法结束本文:
public static void main(String[] args) throws Exception { Class<?> main = Class.forName("cn.annotation.TestAnnotation"); // 打印出全部同类型的注解信息 for (Annotation annotation : main.getAnnotationsByType(B.class)) { System.out.println(annotation); } } 运行结果: @cn.annotation.B(value=1) @cn.annotation.B(value=2) @cn.annotation.B(value=3)