Fork me on GitHub

Java的艺术-注解

此处输入图片的描述
使用注解来进行开发,可以免去写XML配置。那么注解到底是什么呢?

概念

注解(Annotation)

也叫元数据。一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释。
这些标记和注释可以在编译、类加载、运行时被读取,并执行响应的处理。
通俗地讲,注解相当于给类、属性或者方法贴上一个标签。

注解的元注解

元注解是可以注解到注解上的注解,或者说元注解是一种基本注解,但是它能够应用到其它的注解上面。
元注解有五种:

1
@Retention@Documented@Target@Inherited@Repeatable

注解的基本用法

定义

注解通过@interface 关键字定义。

1
2
//创建一个名为AnnotationTest的注解。
public @interface AnnotationTest{}

注解的属性

注解的属性类似于类的成员变量。并且注解只有成员变量,没有方法。注解的成员变量在注解的定义中以“无形参的方法”形式来声明,其方法名定义了该成员变量的名字,其返回值定义了该成员变量的类型。
可以通过default关键字给出默认值。
举例:

1
2
3
4
public @interface AnnotationTest{
int id();
String message() default "Hello World";
}

在使用的时候,赋值的方式是在注解的括号内以 value=””形式,多个属性之间用“,”隔开。

1
2
3
4
5
6
7
8
@AnnotationTest(id=0,message="God")
public class Test{
}
//因为属性message有默认值,也可以不赋值
@AnnotationTest(id=1)
public class Test1{
}
//当所有属性都有默认值,括号内为空

甚至当注解没有属性时,可以不用括号:

1
2
3
4
5
6
public @interface Check {
}
//没有属性的注解,不用括号
@Check
public class Test2{
}

注意

注解中所有方法(也就是属性)没有方法体,且只允许public和abstract修饰。缺省默认为public,且注解方法不允许有throws子句。

注解中方法的返回值只能为:基本数据类型,String,Class,美剧类型,注解和他们的一维数组。

注解的继承只能作用在类上,方法上的注解不会被继承,Interface中的所有注解不会被继承。

常见注解

五种元注解

前面我们提到,Java中有五种元注解。

  • @Retention

    解释说明一个注解的存活时间。取值如下:

    • RetentionPolicy.SOURCE 注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视。
    • RetentionPolicy.CLASS 注解只被保留到编译进行的时候,它并不会被加载到 JVM 中。
    • RetentionPolicy.RUNTIME 注解可以保留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们。
      1
      2
      3
      @Retention(RetentionPolicy.RUNTIME)
      public @interface AnnotationTest {
      }
  • @Documented

能够将注解中的元素包含到 Javadoc 中去。

  • @Target

    限定注解运用的场景。取值如下:

    • ElementType.ANNOTATION_TYPE 可以给一个注解进行注解
    • ElementType.CONSTRUCTOR 可以给构造方法进行注解
    • ElementType.FIELD 可以给属性进行注解
    • ElementType.LOCAL_VARIABLE 可以给局部变量进行注解
    • ElementType.METHOD 可以给方法进行注解
    • ElementType.PACKAGE 可以给一个包进行注解
    • ElementType.PARAMETER 可以给一个方法内的参数进行注解
    • ElementType.TYPE 可以给一个类型进行注解,比如类、接口、枚举
  • @Inherited

    如果一个超类被 @Inherited 注解过的注解进行注解的话,那么如果它的子类没有被任何注解应用的话,那么这个子类就继承了超类的注解。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Inherited
    @Retention(RetentionPolicy.RUNTIME)
    @interface Test {}

    @Test
    public class A {}

    //因为Test注解被@Inherited注解,所以继承了注解了Test的A类后的B类,也拥有Test这个注解。
    public class B extends A {}
  • @Repeatable

    Repeatable 是可重复的意思。@Repeatable 是 Java 1.8 才加进来的,所以算是一个新的特性。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @interface Persons {
    Person[] value();
    }

    @Repeatable(Persons.class)
    @interface Person{
    String role default "";
    }


    @Person(role="artist")
    @Person(role="coder")
    @Person(role="PM")
    public class SuperMan{
    }

@Repeatable 注解了 Person。而 @Repeatable 后面括号中的类相当于一个容器注解。

什么是容器注解呢?就是用来存放其它注解的地方。它本身也是一个注解。

我们再看看代码中的相关容器注解。

1
2
3
@interface Persons {
Person[] value();
}

按照规定,它里面必须要有一个 value 的属性,属性类型是一个被 @Repeatable 注解过的注解数组,注意它是数组。

可以这样理解。Persons 是一张总的标签,上面贴满了 Person 这种同类型但内容不一样的标签。把 Persons 给一个 SuperMan 贴上,相当于同时给他贴了程序员、产品经理、画家的标签。

Java预置注解

Java本身提供了现成的注解。

  • @Deprecated

    用来标记过时的元素。

  • @Override

    提示子类要复写父类中被@Override 修饰的方法

  • @SuppressWarnings

    阻止警告。调用被 @Deprecated 注解的方法后,编译器会警告提醒,而有时候开发者会忽略这种警告,他们可以在调用的地方通过 @SuppressWarnings 达到目的。

  • @SafeVarargs

    参数安全类型注解。它的目的是提醒开发者不要用参数做一些不安全的操作,它的存在会阻止编译器产生 unchecked 这样的警告。它是在 Java 1.7 的版本中加入的。

  • @FunctionalInterface

    函数式接口注解,这个是 Java 1.8 版本引入的新特性。线程开发中常用的 Runnable 就是一个典型的函数式接口。函数式接口可以很容易转换为 Lambda 表达式。

注解的作用

编译检查 编译器可以利用注解来探测错误和警告信息;通过代码里标识的元数据让编译器能够实现基本的编译检查【Override】

编写文档 软件工具可以用来利用注解信息来生成代码、Html文档或者做其它相应处理。通过代码里标识的元数据生成文档【生成文档doc文档】

代码分析 某些注解可以在程序运行的时候接受代码的提取;通过代码里标识的元数据对代码进行分析【使用反射】

总之,当开发者使用了Annotation 修饰了类、方法、Field 等成员之后,这些 Annotation 不会自己生效,必须由开发者提供相应的代码来提取并处理 Annotation 信息。这些处理提取和处理 Annotation 的代码统称为 APT(Annotation Processing Tool)。
而注解主要就是给编译器或者APT用的。

注解也就是代码添加元数据,描述信息。

相比使用单独的XML来描述这些元数据,使用注解要简单些,和代码在一起也更好维护。相比使用继承(如TesCase)或者方法前缀的约定(如testXXX是测试方法)要灵活些。

一些插件机制就是通过注解提供插件的元数据,在加载类后扫描所以带该注解的类就可以找到插件,减少了配置的麻烦。

注解的解析-反射

注解的提取(解析)需要借助于 Java 的反射技术,反射比较慢,所以注解使用时也需要谨慎计较时间成本。

类的注解的解析

首先通过Class对象的isAnnotationPresent()方法判断他是否应用了某个注解:

1
public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass){}

然后通过getAnnotation()方法获取Annotation对象。

1
public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {}

或者getAnnotations()方法:

1
public Annotation[] getAnnotations() {}

如果获取到的 Annotation 如果不为 null,则就可以调用它们的属性方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@AnnotationTest()
public class Test {

public static void main(String[] args) {

boolean hasAnnotation = Test.class.isAnnotationPresent(TestAnnotation.class);

if ( hasAnnotation ) {
TestAnnotation testAnnotation = Test.class.getAnnotation(TestAnnotation.class);

System.out.println("id:"+testAnnotation.id());
System.out.println("message:"+testAnnotation.message());
}

}

}

也可以使用forName()方法加载类,使用isAnnotation(Annotation.class)判定对象是否存在注解,并使用getAnnotation(Description.class)得到带有@Description注解的类。

方法的注解的解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Test {
@AnnotationTest()
public void test_method(String str) {
System.out.println(str);
}

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Test my_test = new Test();
Class test_class = Test.class;
Method method = test_class.getDeclaredMethod("test_method",String.class);
if (method.isAnnotationPresent(AnnotationTest.class)) {
//执行方法
method.setAccessible(true);
method.invoke(my_test, "hello");
//解析注解元素
AnnotationTest a = method.getAnnotation(AnnotationTest.class);
System.out.println(a.test());
}
}
}

类的属性的注解的解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Test {
@AnnotationTest()
public void test_method(String str) {
System.out.println(str);
}

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Test my_test = new Test();
//利用反射,通过class对象的getDeclaredField(String str)获得元素Field对象
Field test_field = Test.class.getDeclaredField("value");
//判断元素是否是注解的对象
if(test_field.isAnnotationPresent(AnnotationTest.class)){
//修改权限(同一个类内部其实没有必要,肯定有访问权限。)
test_field.setAccessible(true);
//利用Field对象的getXXX()获取实例的值。
System.out.println(test_field.getInt(my_test));
}
}
}

注意

一定要用@Retention元注解表示出注解的存活时间,不然在动态执行时可能判定某个类并没有受到注解。

1
2
3
4
@Retention(RetentionPolicy.RUNTIME)
public @interface AnnotationTest {
abstract int test() default 2019;
}


参考1
参考2

-------------本文结束感谢阅读-------------