用影片《记忆碎片》来解释Java注解的工作原理

注解于我们而言并不陌生,遗憾的是,大多数人对于注解的认识,都只停留在使用的层面上,对其背后的原理则知之甚少。

在享受注解所带来的便利的同时,你是否也曾发出过这样的疑问,即:

小小的一个注解,是怎么帮我们完成某件特定的工作的呢?

大家好,我是码仔,今天我们要分享的主题是Java注解的工作原理

在文章开始之前需要先说明的是,本期我们将采用一种比较新颖的讲解方式,即类比的手法,这种手法在我们平时接触和学习一门全新的事物时经常运用。

不同之处在于,本期要类比的一系列事物,来源于一部经典的影视作品——由克里斯托弗·诺兰导演、盖·皮尔斯主演的悬念影片《记忆碎片》。

记忆碎片

毫无疑问这需要读者老爷们看过这部影片,且对影片中的主要情节有大概的认知。没看过的也不要紧,下面会提供一个剧情梗概,以便我们快速掌握该影片的一些背景知识和剧情设定。

剧情梗概

影片的主人公莱尼在一次与入室抢劫歹徒的搏斗中身负重伤,妻子也惨遭杀害。虽然莱尼侥幸活了下来,但却从此患上了一种十分奇特的“短期失忆症”,只能记住受伤之前以及当前最多十分钟之内的事情。

因不满于警方的草草结案,莱尼誓要自己追查到凶手并替爱妻报仇,但支离破碎的记忆却使莱尼举步维艰。他只能不断地借助纸条、照片、纹身上的笔记来记录有价值的线索,告诉自己下一步的目标,因为很可能十分钟后,他就完全记不得自己在哪里,要做什么了。

让我们来提取一下其中的关键词:短期失忆症、复仇、笔记。这里着重介绍一下短期失忆症这个重要设定。

打个比方,就像是我们的App被禁止了往磁盘里写入新的数据,往后的通信都只能依赖内存和旧有的磁盘数据。由于内存不是持久化存储,因此每次App重启后,之前存储在内存的那部分数据就丢失了,App被迫又回到了之前的初始状态。

了解完影片的剧情梗概,我们再来对注解的概念有一个基本的认识。

注解是什么?

官方文档上对于注解(Annotation)的解释如下:

注解是一种元数据形式,提供了与程序相关、但不属于程序本身的数据。注解对它所注解的代码的操作没有直接影响。

嗯…这个措辞可以说是很官方、很专业性了,就是读完之后,不免和记忆刚重启的莱尼一样一脸困惑。

我们提取一下关键的内容重新组织一下:

  1. 注解提供了一些数据用于解释程序
  2. 注解并不会影响程序本身的运行

什么意思呢?我们可以用影片中的重要道具——莱尼的笔记来进行类比。

笔记是莱尼用于应对短期失忆症的道具,解释了莱尼当前所处的地方是在哪里,以及出现在这里的目的,但笔记本身并不会给莱尼叠加什么力量或攻速的Buff。

笔记要真正发挥作用,是需要莱尼在记忆重启后主动地去检查并尝试梳理之后才可以。

注解也是一样,它只提供数据,并不影响程序,真正要依靠注解完成某个功能,还需要我们有一个主动检索注解的步骤。

但在检索之前,我们需要先完成注解的定义与基本应用。

注解的定义与基本应用

想象你就是莱尼本尼,对你来说:

注解的定义,就相当于你每次构思笔记内容的过程;

而注解的应用,则相当于你将其写到纸条、照片或纹到身体的某一处的过程。

回到注解本身。

要定义一个注解,最简单的方式中如下:

public @interface Entity {
}

如你所见,其与接口的定义方式很相似,区别在于interface关键字前面多加了一个@符号,用于向编译器指示这是一个注解

注解的基本应用也很简单,在类、字段、方法等元素的声明前面加上@Xxx即可。根据Java的习惯,每个注解通常要占据单独一行。

@Entity
class MyClass { ... }

不过,光这样还不够,要让想注解真正起作用,我们还需要为注解添加上元注解

元注解是什么?

元注解是应用于其他注解之上的注解

这样说有点拗口,你可以这样理解:

元注解本身也是一个注解,只不过其作用的对象限定为了另外一个注解

就像莱尼的笔记也必须遵循叙事的六要素(时间/地点/人物等)一样,元注解的作用,就是注明了一个注解对象必须包含的基本要素,比如保留时间、作用对象等。

Java内部定义了几种元注解类型:

@Retention

Retention从字面上理解是保持、保留的意思,当@Retention被应用到一个注解之上时,即注明了这个注解的的保留时间

用影片中的一个情节来举例就是:莱尼在吉米的衣服口袋里找到了一条写在杯垫底部的笔记,笔记指示去菲迪斯酒吧找娜塔莉。@Retention元注解就相当于给这条笔记指定了有效时间为“找到娜塔莉为止”,找到娜塔莉之后该笔记就过期失效了。

又比如,莱尼在影片开头自述,说他会把认为重要的事情直接纹在身上,以作为永久备忘。像这一类的笔记,相当于用@Retention元注解指定了笔记的有效时间为”永久“,其将在每次记忆重启后都作为关键线索使用。

理解之后,我们在来看再来看@Retention元注解可能的取值:

  • RetentionPolicy.SOURCE – 注解只在源码阶段保留,编译时将被忽略。
  • RetentionPolicy.CLASS – 注解只被保留到编译阶段,但会被JVM忽略。
  • RetentionPolicy.RUNTIME – 注解由JVM保留,因此可以在运行阶段使用。

保留到不同阶段的注解,有着各自不同的作用,这个我们放到后面再讲。

@Documented

这个元注解的作用,是将其修饰的注解包含到Javadoc中去。

@Target

Target这个单词我们都认识,是目标、靶子的意思,它限定了注解可以应用于哪种Java元素。

这次我们可以用莱尼写在人物照片前后的笔记来类比:

照片1正面写着“泰迪”,背面写着“别相信他的谎言”。@Target元注解就相当于照片正面的人物名字,限定了笔记作用到的目标人物为“泰迪”。

照片2正面写着“娜塔莉”,背面写着“她也失去了爱人,会同情你、帮你”,@Target元注解同样相当于限定了笔记作用到的目标人物为“娜塔莉”。

@Target元注解可能的取值如下:

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

@Inherited

Inherited是继承的意思,但并不是说注解本身可以被继承,而是说如果一个父类被一个包含@Inherited元注解的注解所修饰,那么它的子类如果没有包含任何注解的话,就默认继承了该父类的这个注解。

比如,我们为前一小节的@Entity注解添加@Inherited元注解后,重新应用到MyClass类,之后定义一个MyClass的子类SubClass,那么SubClass默认也将拥有@Entity这个注解:

@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Entity {
}
@Entity
class MyClass { ... }
class SubClass extends MyClass {...}

Java 7之后,又添加多了3个新类型的元注解@SafeVarargs、@FunctionalInterface、@Repeatable,感兴趣的可以去了解一下,这里就不一一展开了。

注解的属性

如果说,元注解指定的是一个注解必须包含的部分,那么关于注解可自定义扩展的部分,则是由注解的属性来指定的。

注解的属性是以“无形参方法”的形式来声明的,其方法名定义了该属性的名字,返回值定义了该属性的类型,可选的类型包括几种基本数据类型外加字符串、类、枚举、注解及它们的数组。

属性可以有默认值,用default关键字指定。

比如以下代码,就为@Author注解声明了2个String类型的属性:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Author {
   String author() default "unknown";
   String date();
}

然后,在为类、方法、字段等元素添加@Author注解时,就可以为这2个属性赋值:

@Author(
   name = "Benjamin Franklin",
   date = "3/27/2003"
)
class MyClass { ... }

另外,如果注解只有一个名为value的属性,那么可以省略该名称:

@SuppressWarnings("unchecked")
void myMethod() { ... }

而如果注解不包含任何属性,则连括号都可以省略了:

@Override
void mySuperMethod() { ... }

注解的提取

在编译Java源代码时,注解可以交由一个叫做注解处理器的编译器插件进行处理。处理器可以生成信息,或创建额外的Java源文件或资源,这些文件或资源又可以反过来再被编译和处理。

这个该怎么理解呢?

有一个情节是这样的,莱尼在制服了达德之后拍下照片,并根据在自己身上摸索出的纸条上面的内容,重新梳理了下一步目标并记录在照片上。

这里的纸条就相当于交给处理器的注解,是一条线索,照片就相当于根据注解额外创建的Java源文件或资源,反过来又可以作为下一步的线索。

除了可以使用注解处理器来处理注解外,由于注解类型和类一样,都会被编译并存储在字节码文件(.class)中,因此我们还可以自己编写代码,使用反射来处理注解。

从Java SE 5开始,与反射相关的java.lang.reflect软件包就为注解定义了一系列的新接口,在Class、Constructor、Field、Method和Package中都有对应的实现,主要的方法有:

  • isAnnotationPresent(Class<? extends Annotation> annotationType) :判断该Java元素是否应用了某个注解

  • getAnnotation(Class<T> annotationClass):获取某个指定类型的注解

  • getAnnotations() :返回这个Java元素上的所有注解

合理利用这几个方法,我们就可以在运行时动态判断指定的Java元素是否包含某个注解,以及根据提取到的注解内容,编写对应的处理逻辑,完成某件特定的工作。

提取操作的演示代码将在《定义并运用自定义注解》一节中给出。

注解的作用

以上内容都掌握了之后,我们再回过头来,讲解保留到不同阶段的注解的作用:

RetentionPolicy.SOURCE

只在源码阶段保留的注解,通常是起代替代码注释的作用。

比如有开发团队会要求在开始对每个类的正式编写之前,必须以注释的形式提供这个类的重要信息。

public class Generation3List extends Generation2List {

   // Author: John Doe
   // Date: 3/17/2002
   // Current revision: 6
   // Last modified: 4/12/2004
   // By: Jane Doe
   // Reviewers: Alice, Bill, Cindy

   // class code goes here

}

我们可以改由注解的形式来实现,为此,我们需要先定义一个注解类型:

@interface ClassPreamble {
   String author();
   String date();
   int currentRevision() default 1;
   String lastModified() default "N/A";
   String lastModifiedBy() default "N/A";
   // Note use of array
   String[] reviewers();
}

然后,就可以在对应类的前面添加该注解,并为该注解的各项属性赋值。

@ClassPreamble (
   author = "John Doe",
   date = "3/17/2002",
   currentRevision = 6,
   lastModified = "4/12/2004",
   lastModifiedBy = "Jane Doe",
   // Note array notation
   reviewers = {"Alice", "Bob", "Cindy"}
)
public class Generation3List extends Generation2List {

// class code goes here

}

我们还可以搭配@Documented元注解,使得该注解包含的信息出现在Javadoc生成的文档中。

RetentionPolicy.CLASS

保留到编译阶段的注解,主要有以下两个作用:

  1. 提供信息给编译器——编译器可以使用注解来检测错误或抑制警告。
  2. 编译阶段时的处理——软件工具可以用来处理注解信息以生成代码、XML文件等。

作用1,我们将在《内置注解》一节中讲到。

作用2,我们将以EventBus框架为例来说明。

EventBus从2.X到3.X最大的变化,就是引入了注解处理器,以解决原先反射获取性能较低的问题。该处理器会在构建时,检索所有注解并生成一个类,该类会包含所有在运行时需要的数据,也就是说耗时的工作都在编译阶段完成了,因而极大地提高了运行阶段的处理速度。

RetentionPolicy.RUNTIME

保留到运行阶段的注解,可以在程序运行的时候接受代码的提取,以实现动态处理——这是我们最常规的用法。

定义并运用自定义注解

如果你读到这里,恭喜你已经掌握了自定义一个注解所需要具备的所有知识了,下面就让我们来实际操作一下,提取一个类注解的数据:

步骤1,定义一个名为TypeHeader的注解,指定保留到运行时:

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

// This is the annotation to be processed
// Default for Target is all Java Elements
// Change retention policy to RUNTIME (default is CLASS)
@Retention(RetentionPolicy.RUNTIME)
public @interface TypeHeader {
    // Default value specified for developer attribute
    String developer() default "Unknown";
    String lastModified();
    String [] teamMembers();
    int meaningOfLife();
}

步骤2,将注解应用与某个类上,并为注解声明的各项属性赋值:

// This is the annotation being applied to a class
@TypeHeader(developer = "Bob Bee",
    lastModified = "2013-02-12",
    teamMembers = { "Ann", "Dan", "Fran" },
    meaningOfLife = 42)

public class SetCustomAnnotation {
    // Class contents go here
}

步骤3,获取该类的Class对象,先调用Class对象的isAnnotationPresent方法,判断是否存在@TypeHeader注解;如果存在,再调用getAnnotation方法获取@TypeHeader注解并打印注解的属性:

// This is the example code that processes the annotation
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;

public class UseCustomAnnotation {
    public static void main(String [] args) {
        Class<SetCustomAnnotation> classObject = SetCustomAnnotation.class;
        readAnnotation(classObject);
    }

    static void readAnnotation(AnnotatedElement element) {
        try {
            System.out.println("Annotation element values: \n");
            if (element.isAnnotationPresent(TypeHeader.class)) {
                // getAnnotation returns Annotation type
                Annotation singleAnnotation = 
                        element.getAnnotation(TypeHeader.class);
                TypeHeader header = (TypeHeader) singleAnnotation;

                System.out.println("Developer: " + header.developer());
                System.out.println("Last Modified: " + header.lastModified());

                // teamMembers returned as String []
                System.out.print("Team members: ");
                for (String member : header.teamMembers())
                    System.out.print(member + ", ");
                System.out.print("\n");

                System.out.println("Meaning of Life: "+ header.meaningOfLife());
            }
        } catch (Exception exception) {
            exception.printStackTrace();
        }
    }
}

内置注解

除了可以自定义注解,Java API本身也内置了几个现成可用的注解,这里列举几个常见的:

@Deprecated

这个注解用于表示其所标记的元素已被弃用,不应再使用。每当程序使用带有@Deprecated注解的方法、类或字段时,编译器都会生成警告。

通常还要搭配Javadoc的@deprecated标签进行记录,解释其为什么被弃用:

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

@Override

这个注解用于通知编译器,其所标记的元素旨在覆盖父类中声明的元素,比如方法、字段等:

   // mark method as a superclass method
   // that has been overridden
   @Override 
   int overriddenMethod() { }

虽然我们在重写方法时,并没要求必须使用此注解,但它有助于防止错误情况的发生。比如被标记为@Override的方法如果在父类中实际不存在,编译器将提示错误。

@SuppressWarnings

这个注解用于让编译器抑制特定的警告。比如当我们使用了Java API不建议使用的方法(比如被弃用的方法)时,编译器就会生成警告。而当我们在该方法前添加@SuppressWarnings注解后,该警告就会被抑制:

   // use a deprecated method and tell 
   // compiler not to generate a warning
   @SuppressWarnings("deprecation")
    void useDeprecatedMethod() {
        // deprecation warning
        // - suppressed
        objectOne.deprecatedMethod();
    }

简直是强迫症患者的福音了。

好了,以上就是今天要分享的内容,现在我们可以来回答开篇的那个问题了:

  • 注解只是提供了数据,本身并不会做任何事情。因此,单纯添加注解,并不会影响程序的运行;
  • 真正要依靠注解完成某个功能,还须得有一个主动检索注解的步骤;
  • 检索注解就是一个提取注解自定义属性的过程,根据提取结果的不同编写对应的处理逻辑代码;
  • 检索注解的时机由@Retention元注解决定,该元注解指定了其修饰的注解将保留到哪个阶段;
  • 保留到编译阶段,则是交由了注解处理器处理;
  • 保留到运行阶段,则是利用反射机制进行提取。

少侠,请留步!若本文对你有所帮助或启发,还请:

  1. 点赞👍🏻,让更多的人能看到!
  2. 收藏⭐️,好文值得反复品味!
  3. 关注➕,不错过每一次更文!

===> 技术号:「星际码仔」💪

你的支持是我继续创作的动力,感谢!🙏

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

推荐阅读更多精彩内容