美文网首页程序员
用影片《记忆碎片》来解释Java注解的工作原理

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

作者: 星际码仔 | 来源:发表于2022-10-17 07:37 被阅读0次

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

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

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

    大家好,我是码仔,今天我们要分享的主题是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元注解决定,该元注解指定了其修饰的注解将保留到哪个阶段;
    • 保留到编译阶段,则是交由了注解处理器处理;
    • 保留到运行阶段,则是利用反射机制进行提取。

    相关文章

      网友评论

        本文标题:用影片《记忆碎片》来解释Java注解的工作原理

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