夯实基础:Java的注解

前言

本文是系列文章的第二篇,Java的注解。个人建议先读完第一篇夯实基础:Java的反射,因为在本文的后半部分,将使用到一些反射的技术,学完了反射再学本文的内容更有助于你理解注解,当然,你不学或者不会反射,也不会对你学习本文的内容造成太大影响,希望大家结合自身的情况进行选择。

注解的概念

首先注解不是注释。注释大家都知道是给我们开发者看的,而注解呢是给程序看的。我们可以把注解理解为标签,这些标签可以用在Java的类、成员变量、成员方法、构造方法、形参、局部变量等等程序属性上面,并且能够在Java文件、编译期和运行时被读取,开发者可以在程序逻辑不被修改的情况下对代码嵌入补充信息。

Java内置的注解

java给我们内置提供了几个注解,下面我们分别看一下

  • @Override:验证子类是否重写了父类的方法。该注解仅在Java代码时有效,编译阶段就会被丢弃
  • @Deprecated:验证变量、方法等程序元素是否过时,注意这里过时不代表不可以被使用,只是有了更好的替代。该注解会一直保留到运行时
  • SuppressWarnings:压制警告,里面需要接收一个value参数来表明你要压制哪种警告。该注解的有效期同@Override,仅在Java代码时有效,编译阶段就会被丢弃
  • @SafeVarargs:压制堆污染警告,保留到程序运行时,仅对构造方法和成员方法有效
  • @Functionallnterface:作用在接口上,保证这个接口只有一个抽象方法,一直保留到程序运行时

元注解

想了解注解之前,必须要知道什么是元注解。所谓元注解就是注解的注解,本身就是一个注解,用来修饰注解的,先来认识几个java内置的元注解

  • @Target:目标对象,就是说你这个注解对谁起作用,看一眼源码
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    /**
     * Returns an array of the kinds of elements an annotation type
     * can be applied to.
     * @return an array of the kinds of elements an annotation type
     * can be applied to
     */
    ElementType[] value();
}

里面有一个value属性,返回的是ElementType[],看一下ElementType的取值

public enum ElementType {
    /** Class, interface (including annotation type), or enum declaration */
    TYPE,

    /** Field declaration (includes enum constants) */
    FIELD,

    /** Method declaration */
    METHOD,

    /** Formal parameter declaration */
    PARAMETER,

    /** Constructor declaration */
    CONSTRUCTOR,

    /** Local variable declaration */
    LOCAL_VARIABLE,

    /** Annotation type declaration */
    ANNOTATION_TYPE,

    /** Package declaration */
    PACKAGE,

    /**
     * Type parameter declaration
     *
     * @since 1.8
     */
    TYPE_PARAMETER,

    /**
     * Use of a type
     *
     * @since 1.8
     */
    TYPE_USE
}

取值基本都是包、类、成员方法、成员变量、构造方法、局部变量等等的程序属性

  • @Retention:中文为保留,就是说注解保留到什么阶段,从什么阶段到什么阶段有效。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
    /**
     * Returns the retention policy.
     * @return the retention policy
     */
    RetentionPolicy value();
}

里面有一个属性value,返回对象是RetentionPolicy,这是个枚举类,里面有三个枚举值,SOURCE,CLASS,RUNTIME
SOURCE:java源代码阶段
CLASS:把java文件编译成class文件阶段
RUNTIME:程序运行时,基本就等于一直存在,我们绝大多数的时候都用这个阶段

  • @Documented:作用在类上,被@Documented标记的类,使用javadoc命令执行一下对应的类就会生成文档,相对来说这个注解用的情况比较少
  • @Inherited:作用在子类上,被@Inherited标记的子类会继承父类的注解,一般用的也比较少
    注意:Java内置的注解还有@Native、@Repeatable以及@Annotation,这些不是很常用,感兴趣可以自行google一下,上面4个注解,其中@Target和@Retention是如何注解都必须要设置的,一定要记住。

注解的本质

介绍完了元注解,我们现在来了解一下注解的本质是什么。我这里先创建了一个注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface TestAnno {
}

通过javac编译生成TestAnno.class,然后再用javap反编译一下TestAnno.class

反编译注解.png

直接看图,我们发现我们创建的TestAnno实际上一个继承了Annotation的接口,Annotion也是一个接口,它是所有注解的父类,到现在我们弄明白了注解的本质就是一个接口。
既然注解是一个接口,那我们就可以用对待接口的方式对待注解,接口里面有抽象方法,我们就可以在注解里面创建抽象方法

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface TestAnno {
     String name();
     int age();
}

使用一下这个注解

@TestAnno(name = "张三",age = 22)
public class Person extends Object {

用使用前可以看出,我们创建的是抽象方法,但是在实际用的时候好像跟属性一样,都是XX属性=xxx,其实注解里面的抽象方法就是来描述这个注解的属性的,所以我们在给方法命名的时候最好也按照属性命名。我们如果要使用的注解的话需要给里面的属性赋值,像“@TestAnno(name = "张三",age = 22)”这种,如果实在不想赋值的话,可以在创建属性的时候给默认值

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface TestAnno {
     String name() default "张三";
     int age() default 22;
}

所有的注解还有一个默认的属性value,当你使用了value属性,在赋值的时候可以不写前面“value=”

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface TestAnno {
    String name() default "张三";
    int age() default 22;
    String value();
}

@TestAnno("555")//这里的555是给value赋值
public class Person extends Object {

注意:注解里面的抽象方法(属性)的返回值,只能是:基本数据类型、String类型、注解类型以及它们的数组,不能是自定义的对象类型以及void,比如

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface TestAnno {
    Person person();//编译器就直接报错了
   void test();//不被允许
}

自定义注解

了解了注解的本质以后,我们来自定义一个注解。我们知道修饰类的关键字是class,接口的是interface,枚举的是enum,而修饰注解的就是@interface
注解还必须有@Target和@Retention,举个例子

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.FIELD,ElementType.METHOD})
public @interface TestAnno2  {
    String value();
    int[] ids();
    TestAnno anno();
    
}

其实了解了注解的本质以后,自己写个注解根本不是事,然而自定义注解根本不是关键,因为现在这个注解其实没有任何意义。所以我们要解析这些注解,如果解析呢?这里就用到我们上一篇文章学到的反射了。

利用反射解析注解

首先,解释一下为什么要通过反射来解析注解。注解是作用在包、类、变量、方法等程序属性上,如果我们要想拿到注解,就必须先得拿到这些程序属性,而如何能拿到这些程序属性呢?正是通过反射!
下面我将以一个具体的例子来讲解一下

/**
 * 加减乘除
 * */
public class MathCalculation {

    @CheckMath
    public int add(int a,int b) {
        return a+b;
    }

    @CheckMath
    public int sub(int a,int b) {
        return a-b;
    }

    @CheckMath
    public int mul(int a,int b) {
        return a*b;
    }

    @CheckMath
    public int exc(int a,int b) {
        return a/b;
    }
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CheckMath {//用来检查加减乘除四个运算
    int[] aList() default {1,2,3,4,5,6,7,8,9,0};//默认值1~9
    int[] bList() default {1,2,3,4,5,6,7,8,9,0};
}

上面两个代码很简单,我现在需要用CheckMath检查一下加减乘除这四个方法在a和b分别是1~9的情况下是否正确

public class Test {

    public static void main(String[] args)  throws Exception{
        Class mathCalculationClass = MathCalculation.class;//拿到MathCalculation class类对象
        MathCalculation mathCalculation = mathCalculationClass.newInstance();
        Method[] methods = mathCalculationClass.getMethods();//获取MathCalculation所有的public方法
        for (Method method:methods) {//遍历所有的public方法
            if (method.isAnnotationPresent(CheckMath.class)) {//判断该方法是否有CheckMath.class
                CheckMath checkMath = method.getAnnotation(CheckMath.class);//获取CheckMath注解对象
                int[] aList = checkMath.aList();//获取a的数组
                int[] bList = checkMath.bList();//获取b的数组
                for (int a:aList) {
                    for (int b:bList) {
                        try {
                            method.invoke(mathCalculation,a,b);//调用计算方法
                        }catch (Exception e) {
                            //出错以后打印log
                            System.out.println("出现错误"+"a= "+a+",b="+b+" 错误原因:"+e.getCause());
                        }

                    }
                }
            }
        }
    }
}

基本上每一行都有注释了,这里就不再赘述,看一眼打印结果

运行结果.jpg

当b为0报了数学异常,因此咱们的CheckMath注解还是发挥了它的作用。
注意:这里我们先用反射拿到了程序属性,再通过程序属性拿到了注解。反射拿到程序属性咱们上一节说过,那为什么程序属性就能拿到注解呢?这个其实很简单,我们打开类似Packge、Class、Constructor、Filed、Method这些程序属性类的源码会发现他们都实现了一个叫AnnotatedElement的接口,在这个AnnotatedElement接口里面定义了跟注解相关的方法,核心常用的有三个

  • default boolean isAnnotationPresent(Class annotationClass) {
    return getAnnotation(annotationClass) != null;
    } :判断程序属性是否被某个注解标记
  • T getAnnotation(Class annotationClass):获取指定的注解对象
  • Annotation[] getAnnotations():获取所有的注解对象

总结:

注解是对程序信息的一种补充标记,本质上是一个特殊的接口,接口里面定义的方法实际上是注解的属性。注解单独使用没有任何意义,必须要结合反射来解析。解析的本质是先通过反射拿到程序信息,再通过程序信息拿到注解对象,而程序信息可以拿到注解对象是因为程序信息实现了AnnotatedElement接口。

你可能感兴趣的:(夯实基础:Java的注解)