Java 之路 (十九) -- 注解(语法、预定义注解、元注解、重复注解、注解与反射)

前言

官方注解的定义如下:

注解(一种元数据形式)提供有关不属于程序本身的程序的数据。注解对它们注解的代码的操作没有直接影响。

注解有许多用途,其中包括:

  • 编译器的信息 - 编译器可以使用注解来检测错误或抑制警告。
  • 编译时和部署时处理 - 软件工具可以处理注解信息以生成代码,XML文件等。
  • 运行时处理 - 可以在运行时检查某些注解。

第一次看时候觉得"这尼玛是什么",于是换个简单一点的说法。

我们可以把注解当作是“标签”,就像是我们对某个人的看法一样,这个“标签”包含了对其中属性的评价(注解元素)。我们就可以对某个程序元素贴上这种"标签"(使用注解),既然能贴上去,那么我们自然也能通过某种方式(反射)来将这个"标签"撕下去(获取注解)。

有了这种比喻,相信下面对于注解的讲解就能更更容易理解了。一旦觉得理解不了注解是个什么东西,就回想“注解 ≈ 标签”就好了。


1. 基础知识

1.1 注解的格式

  1. 注解中无元素:

    @Entity
    
    //如下,用于重写的 @Override 注解
    @Override
    void myMethod(){...}
    
  2. 注解中有多个元素,且这些元素有值:

    //这里的 @Author 是假定的自定义注解
    @Author(
       name = "Benjamin Franklin",
       date = "3/27/2003"
    )
    class MyClass() { ... }
    
  3. 注解中只有一个元素时:

    //@SuppressWarnings 该注解是 Java 预定义的注解
    
    @SuppressWarnings(value = "unchecked")
    void myMethod() { ... }
    
    //或者 可以将 value 省略
    @SuppressWarnings("unchecked")
    void myMethod() { ... }
    
  4. 可以在同一声明中添加多个注解:

    @Author(name = "Jane Doe")
    @EBook
    class MyClass { ... }
    
  5. 可以使用重复注解(使用相同类型的注解)

    @Author(name = "Jane Doe")
    @Author(name = "John Smith")
    class MyClass { ... }
    

1.2 可以使用注解的位置

注解可以应用于声明以下内容:

  • 字段
  • 方法
  • 其他程序元素

在 Java SE 8 以前,注解只能应用于声明。从 Java SE 8开始,添加了类型注解,具体示例如下:

  • 创建类实例

    new @Interned MyObject();
    
  • 类型转换

    myString = (@NonNull String) str;
    
  • implements 子句

    class UnmodifiableList<T> implements @Readonly List<@Readonly T> { ... }
    
  • 异常抛出语句

    void monitorTemperature() throws @Critical TemperatureException { ... }
    

2. 声明注解类型

注解类型定义类似于接口定义,通过使用 @interface 关键字进行定义。

@interface MyAnnotation{
    
}

前面我们提到过,注解中可以有元素,注解元素是通过以下方式定义的:

@interface MyAnnotation{
    int id() default 0;
    String msg();
    //...
}

这样定义好之后,我们就可以使用该类型的注解,并填入值,如下例子:

@MyAnnotation(id = 3, msg = "hello annotation")
public class Test {
    //...
}

在定义 @MyAnnotation 注解时,我使用了一个关键字 default,它是用来指定默认值的。当指定默认值之后,在我们使用注解并不填入值时,Java 会自动将其设为默认值。于是我们就可以如下调用:

@MyAnnotation(msg = "hello annotation")
public class Test {
    //...
}

这里需要注意注解元素的类型是有限制的,注解元素只能使用如下类型:

  • 基本数据类型
  • String
  • Class
  • enum
  • Annotation
  • 以上类型的数组

3. 预定义注解

3.1 Java 使用的预定义注解

Java 使用的注解类型有如下几种:

  1. @Deprecated

    表示被注解的元素已弃用,不应再使用,只要程序使用带有 @Deprecated 注解的方法/类/字段,编译器就会生成警告。当不推荐使用时,也应使用 Javadoc @deprecated 对其标记。

       // Javadoc comment follows
        /**
         * @deprecated
         * explanation of why it was deprecated
         */
        @Deprecated
        static void deprecatedMethod() { }
    
  2. @Override

    该注解通知编译器被注解的元素要覆盖超类中声明的元素,具体实际上就是重写方法时使用。

       // 将方法标记为已将超类方法覆盖
       @Override 
       int overriddenMethod() { }
    

    需要强调一点:实际上,重写方法时 @Override 并非必须的,之所以使用该注解是为了防止出错。

    当使用 @Override 注解的方法并没有正确覆盖其超类方法时,编译器会报错。

  3. @SuppressWarnings

    该注解告知编译器禁止可能生成的特定警告。举个例子,下面的示例中,使用了启用的方法,通常编译器会生成警告,但是使用 @SuppressWarnings 注解后,会导致该警告被禁止生成。

    @SuppressWarnings("deprecation")
        void useDeprecatedMethod() {
            // deprecation warning
            // - suppressed
            objectOne.deprecatedMethod();
        }
    

    关于编译器警告,Java 语言规范指定了两个类别:deprecationunchecked

    deprecation:使用了弃用方法时的类别

    unchecked:当与泛型出现之前编写的遗留代码进行交互时,可能出现的警告类别。

  4. @SafeVarargs

    当该注解应用于方法或构造函数时,它保证了不会对可变参数执行不安全的操作。当使用该注解后,有关 可变参数的 unchecked 警告将被禁止生成。

  5. @FunctionalInterface

    在Java SE 8中引入的,指出类型声明的目的是作为一个函数式接口注解

3.2 元注解

适用于其他注解的注解称为元注解

  1. @Retention

    指定注解的存储方式:

    1. RetentionPolicy.SOURCE - 标记的注解仅保留在源级别中,并被编译器忽略。
    2. RetentionPolicy.CLASS - 标记的注解在编译时由编译器保留,但Java虚拟机(JVM)会忽略。
    3. RetentionPolicy.RUNTIME - 标记的注解由JVM保留,因此运行时环境可以使用它。
  2. @Documented

    作用是能够将注解中的元素包含到 Javadoc 中去

  3. @Target

    限制可以应用注解的 Java 元素类型:

    • ElementType.ANNOTATION_TYPE可以应用于注解类型。

    • ElementType.CONSTRUCTOR可以应用于构造函数。

    • ElementType.FIELD可以应用于字段或属性。

    • ElementType.LOCAL_VARIABLE可以应用于局部变量。

    • ElementType.METHOD可以应用于方法级注释。

    • ElementType.PACKAGE可以应用于包声明。

    • ElementType.PARAMETER可以应用于方法的参数。

    • ElementType.TYPE可以应用于类的任何元素。

  4. @Inherited

    表明注解类型可以从超类继承。当超类使用了被" @Inherited 注解了的" 注解后,如果它的子类没有添加任何注解,那么子类会继承超类的注解。

    比较绕,举个例子:

    @Inherited
    @interface Test {}
    
    
    @Test
    public class A {}
    
    
    public class B extends A {}
    

    上面,@Test 注解是可以被继承的,A 使用了 @Test 注解,B 继承 A,且未添加任何注解,于是 B 也就拥有 @Test 这个注解。

  5. @Repeatable

    Java SE 8 加入的特性,表明被注解的注解可以多次应用于相同的声明。

    具体参见 重复注解 小节。


4. 重复注解

Java SE 8 中,加入了重复注解,这使得我们可以将相同的注解应用于声明或类型引用。

它的声明包含两个步骤:

  1. 声明可重复的注解类型
  2. 声明包含的注解类型

下面通过一个例子来介绍具体如何声明和使用重复注解:

第1步:声明可重复的注解类型

注解类型必须使用 @Repeatable 元注解进行标记:

@Repeatable(Schedules.class)
public @interface Schedule {
  String dayOfMonth() default "first";
  String dayOfWeek() default "Mon";
  int hour() default 12;
}

其中, @Repeatable 元注解的值是Java编译器为存储重复注解而生成的容器注解的类型。此例中,包含的注解类型是 Schedules,因此 @Schedule 注解存储在 @Schedules 注解中。

第2步:声明包含的注解类型

包含的注解类型必须带有数组类型的 value 元素,数组的类型必须是可重复的注解类型:

public @interface Schedules {
    Schedule[] value();
}

5. 检索 (Retrieving) 注解 - 注解与反射

前面我们所有讲解的都是如何声明和使用注解,那么此处就来讲讲怎么获取注解。

这主要是通过 反射 实现的,主要涉及以下 AnnotatedElement 接口提供的抽象方法 :

  1. 查询是否应用了某个注解

    default boolean isAnnotationPresent(Class<? extends Annotation> annotationClass)
    
  2. 获取注解

    方法 功能 说明
    <T extends Annotation> T getAnnotation(Class<T> annotationClass) 返回该程序元素上存在的指定类型的注解,如果该类型的注解不存在,则返回 null 泛型参数表明,只能说注解类型或者某注解类型的子类
    Annotation[] getAnnotations() 返回该元素上存在的所有注解,包括继承得到的。如果没有注解,那么返回长度为零的数组
    default <T extends Annotation> T[] getAnnotationsByType(Class<T> annotationClass) Java SE 8 新增方法,用来获得修饰该程序元素的、指定类型的多个注解
    default <T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass) 返回直接修饰该程序元素、指定类型的注解。如果该类型注解不存在,返回 null 忽略继承得到的注解
    default <T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass) Java SE 8 新增方法,用来获得直接修饰该程序元素的、指定类型的多个注解 忽略继承得到的注解
    Annotation[] getDeclaredAnnotations() 返回直接存在于此元素上的所有注解。如果没有注解,返回长度为零的数组。 忽略继承得到的注解

下面具体来看个例子:

MyAnnotation.java

@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)//指定注解保存在JVM 中,运行时可使用
@Target({ElementType.TYPE,ElementType.FIELD,ElementType.METHOD})//注解可以应用于类、方法和字段上
public @interface MyAnnotation{ 
    String type() default "ignore";
    String[] hobby();
}

TestAnnotation.java

@MyAnnotation(type = "class",hobby = {"sleep","play"})
public class TestAnnotation {
    
    @MyAnnotation(type = "Field",hobby = {"read"})
    private String whdalive;
    
    @MyAnnotation(type = "Field",hobby = {"piano"})
    private String cy;
    
    @MyAnnotation(type = "Method",hobby = {"guitar"})
    public void method1(){
        
    }
    
    public static void main(String[] args) {
        
        //反射获取类对象
        Class<TestAnnotation> clz = TestAnnotation.class;
        //类上是否含有指定类型注解
        boolean clzHasAnno = clz.isAnnotationPresent(MyAnnotation.class);
        //如果有,获取注解元素的值
        if(clzHasAnno) {
            MyAnnotation annotation = clz.getAnnotation(MyAnnotation.class);
            String type = annotation.type();
            String[] hobby = annotation.hobby();
            System.out.println(clz.getName() + ", type = " + type + ", hobby = " + Arrays.asList(hobby).toString());
        }
        //反射获取字段
        Field[] fields = clz.getDeclaredFields();
        for(Field field : fields) {
            boolean fieldHasAnno = field.isAnnotationPresent(MyAnnotation.class);
            if (fieldHasAnno) {
                MyAnnotation annotation = field.getAnnotation(MyAnnotation.class);
                String type = annotation.type();
                String[] hobby = annotation.hobby();
                System.out.println(field.getName() + ", type = " + type + ", hobby = " + Arrays.asList(hobby).toString());
            }
        }
        //反射获取方法
        Method[] methods = clz.getDeclaredMethods();
        for (Method method : methods) {
            boolean methodHasAnno = method.isAnnotationPresent(MyAnnotation.class);
            if (methodHasAnno) {
                MyAnnotation annotation = method.getAnnotation(MyAnnotation.class);
                String type = annotation.type();
                String[] hobby = annotation.hobby();
                System.out.println(method.getName() + ", type = " + type + ", hobby = " + Arrays.asList(hobby).toString());
            }
            
        }
    }
}

输出结果

com.whdalive.reflection.TestAnnotation, type = class, hobby = [sleep, play]
whdalive, type = Field, hobby = [read]
cy, type = Field, hobby = [piano]
method1, type = Method, hobby = [guitar]

总结

注解是 Java 引入的一个非常受欢迎的机制,它提供了一种结构化的并且具有类型检查能力的新途径。我们可以通过注解为代码加入元数据,而不会导致代码杂乱难以阅读。

同时,注解还是 Android 诸多开源框架的实现基础。比如 ButterKnife、Retrofit、Dagger2 等等等等。

注解还是很有必要好好学习一下的。

共勉。

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

推荐阅读更多精彩内容

  • 什么是注解(Annotation):Annotation(注解)就是Java提供了一种元程序中的元素关联任何信息和...
    九尾喵的薛定谔阅读 3,139评论 0 2
  • 本文章涉及代码已放到github上annotation-study 1.Annotation为何而来 What:A...
    zlcook阅读 29,118评论 15 116
  • MyMVVM ARouter + DataBinding + MVVM 的使用演示 关于我 github cs...
    xuexiangjys阅读 429评论 0 1
  • html5与html4的对比 标签 -header-----页面的头部 -main------页面的主体内容<m...
    miss_ca93阅读 212评论 0 1
  • 在恍惚之中,也快到了而立之年,或许尚早,却发觉身边早已物是人非,可惜我爱上了baby,天真的,美丽的,动人的,捉摸...
    Dimm阅读 150评论 0 0