夯实基础: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<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接口。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,911评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,014评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,129评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,283评论 1 264
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,159评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,161评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,565评论 3 382
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,251评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,531评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,619评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,383评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,255评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,624评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,916评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,199评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,553评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,756评论 2 335

推荐阅读更多精彩内容