夯实基础:Java的注解

作者: 肖邦kaka | 来源:发表于2020-03-05 21:39 被阅读0次

    前言

    本文是系列文章的第二篇,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<MathCalculation> 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<? extends Annotation> annotationClass) {
      return getAnnotation(annotationClass) != null;
      } :判断程序属性是否被某个注解标记
    • <T extends Annotation> T getAnnotation(Class<T> annotationClass):获取指定的注解对象
    • Annotation[] getAnnotations():获取所有的注解对象

    总结:

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

    相关文章

      网友评论

        本文标题:夯实基础:Java的注解

        本文链接:https://www.haomeiwen.com/subject/chackhtx.html