Explain how Java annotations work with the film Memento

Annotations are not unfamiliar to us. Unfortunately, most people's understanding of annotations only stays at the level of use, and they know very little about the principles behind them.

While enjoying the convenience brought by annotations, have you ever issued such a question, namely:

How does a small annotation help us accomplish a specific job?

Hello everyone, I'm Codeboy, and the topic we're going to share today is how Java annotations work .

What needs to be explained before the article starts is that in this issue, we will use a relatively novel way of explaining, that is, the method of analogy , which is often used when we usually come into contact with and learn a new thing.

The difference is that the series of things to be compared in this issue comes from a classic film and television work - the suspense film "Memento" directed by Christopher Nolan and starring Guy Pearce.

Undoubtedly, this requires readers to have seen the film and have a general understanding of the main plot of the film. It doesn't matter if you haven't seen it, the following will provide a plot synopsis so that we can quickly grasp some background knowledge and plot settings of the film.

plot synopsis

The film's protagonist, Lenny, was seriously injured in a fight with a burglar, and his wife was killed. Although Lenny survived by luck, he has since suffered from a very strange "short-term amnesia", which can only remember things before the injury and up to ten minutes now.

Dissatisfied with the police's hasty closure of the case, Lenny vows to track down the murderer and avenge his beloved wife, but the fragmented memory makes Lenny difficult. He can only keep using notes, photos, and notes on tattoos to record valuable clues to tell himself what to do next, because it is likely that after ten minutes, he will not be able to remember where he is or what he is going to do at all.

Let's extract the keywords: short-term amnesia, revenge, notes. Here we focus on the important setting of short-term amnesia .

For example, it is like our App is prohibited from writing new data to the disk, and future communication can only rely on memory and old disk data. Since memory is not persistent storage, each time the app is restarted, the part of the data previously stored in memory is lost, and the app is forced to return to its previous initial state.

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

注解是什么?

官方文档上对于注解(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 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元注解决定,该元注解指定了其修饰的注解将保留到哪个阶段;
  • 保留到编译阶段,则是交由了注解处理器处理;
  • 保留到运行阶段,则是利用反射机制进行提取。

Guess you like

Origin juejin.im/post/7155258384667639839