注解于我们而言并不陌生,遗憾的是,大多数人对于注解的认识,都只停留在使用的层面上,对其背后的原理则知之甚少。
在享受注解所带来的便利的同时,你是否也曾发出过这样的疑问,即:
小小的一个注解,是怎么帮我们完成某件特定的工作的呢?
大家好,我是码仔,今天我们要分享的主题是Java注解的工作原理。
在文章开始之前需要先说明的是,本期我们将采用一种比较新颖的讲解方式,即类比的手法,这种手法在我们平时接触和学习一门全新的事物时经常运用。
不同之处在于,本期要类比的一系列事物,来源于一部经典的影视作品——由克里斯托弗·诺兰导演、盖·皮尔斯主演的悬念影片《记忆碎片》。
毫无疑问这需要读者老爷们看过这部影片,且对影片中的主要情节有大概的认知。没看过的也不要紧,下面会提供一个剧情梗概,以便我们快速掌握该影片的一些背景知识和剧情设定。
剧情梗概
影片的主人公莱尼在一次与入室抢劫歹徒的搏斗中身负重伤,妻子也惨遭杀害。虽然莱尼侥幸活了下来,但却从此患上了一种十分奇特的“短期失忆症”,只能记住受伤之前以及当前最多十分钟之内的事情。
因不满于警方的草草结案,莱尼誓要自己追查到凶手并替爱妻报仇,但支离破碎的记忆却使莱尼举步维艰。他只能不断地借助纸条、照片、纹身上的笔记来记录有价值的线索,告诉自己下一步的目标,因为很可能十分钟后,他就完全记不得自己在哪里,要做什么了。
让我们来提取一下其中的关键词:短期失忆症、复仇、笔记。这里着重介绍一下短期失忆症这个重要设定。
打个比方,就像是我们的App被禁止了往磁盘里写入新的数据,往后的通信都只能依赖内存和旧有的磁盘数据。由于内存不是持久化存储,因此每次App重启后,之前存储在内存的那部分数据就丢失了,App被迫又回到了之前的初始状态。
了解完影片的剧情梗概,我们再来对注解的概念有一个基本的认识。
注解是什么?
官方文档上对于注解(Annotation)的解释如下:
注解是一种元数据形式,提供了与程序相关、但不属于程序本身的数据。注解对它所注解的代码的操作没有直接影响。
嗯…这个措辞可以说是很官方、很专业性了,就是读完之后,不免和记忆刚重启的莱尼一样一脸困惑。
我们提取一下关键的内容重新组织一下:
- 注解提供了一些数据用于解释程序。
- 注解并不会影响程序本身的运行。
什么意思呢?我们可以用影片中的重要道具——莱尼的笔记来进行类比。
笔记是莱尼用于应对短期失忆症的道具,解释了莱尼当前所处的地方是在哪里,以及出现在这里的目的,但笔记本身并不会给莱尼叠加什么力量或攻速的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
保留到编译阶段的注解,主要有以下两个作用:
- 提供信息给编译器——编译器可以使用注解来检测错误或抑制警告。
- 编译阶段时的处理——软件工具可以用来处理注解信息以生成代码、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元注解决定,该元注解指定了其修饰的注解将保留到哪个阶段;
- 保留到编译阶段,则是交由了注解处理器处理;
- 保留到运行阶段,则是利用反射机制进行提取。
少侠,请留步!若本文对你有所帮助或启发,还请:
- 点赞👍🏻,让更多的人能看到!
- 收藏⭐️,好文值得反复品味!
- 关注➕,不错过每一次更文!
===> 技术号:「星际码仔」💪
你的支持是我继续创作的动力,感谢!🙏