夯实 Java 基础 - 注解
不知道大家有没有一种感觉,当你想要了解某个知识点的时候,就会发现好多技术类 APP 或者公众号在推一些关于这个知识点的文章。也许这就是大数据的作用,这也说明总有人比你抢先一步。学习不能停滞,要不你就会被别人越落越远。
本文接着来回顾和总结 Java 基础中注解的知识点和简单的使用,同样本文将从以下几个方面来回顾注解知识:
- 注解的定义
- 注解的语法
- 源码级别的注解的使用
- 运行时注解的使用
- 编译时注解的使用
- Android 预置的注解
注解的定义
注解(Annotation),也叫元数据。一种代码级别的说明。它是 JDK 1.5 以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等元素上。它提供数据用来解释程序代码,但是注解并非是所解释的代码本身的一部分。注解对于代码的运行效果没有直接影响。
注解有许多用处,主要如下:
- 提供信息给编译器: 编译器可以利用注解来探测错误和警告信息
- 编译阶段时的处理: 软件工具可以用来利用注解信息来生成代码、Html 文档或者做其它相应处理。
- 运行时的处理: 某些注解可以在程序运行的时候接受代码的提取
如我们所熟知的依赖注入框架 ButterKnife
就是在编译阶段来生成 findViewById
的代码(文件)的,而我们所见过的 @Deprecated
就是提供信息给编辑器的RetentionPolicy.SOURCE
类型注解,说明这个属性已经过时的,对于运行时的注解在反射的文章的最后我们也举了个小例子,说明了它的作用。
在自定义了一个编译或者运行阶段的注解后,需要一个开发者编写相应的代码来解释这些注解,从而来发挥注解的作用。这些用来解释注解的代码被统称为是 APT(Annotation Processing Tool)
。换句话说注解其实是给 APT 或者编辑器来使用的,而对于非框架开发人员的我们我们只需要关注注解的使用,并遵守规则即可,从而我们节省了很多代码提高了效率。
但是凡事如果只满足于用上,就不算是一个合 (tong) 格 (guo) 程 (mian)序 (shi) 员 (de)! 但是不要慌,当你打开这篇文章的时候你已经离 offer 又进了一步。
注解的语法
注解的声明
注解的声明和声明一个接口十分类似,没错只是名字很类似~ 我们使用@interface
来声明一个注解,如我们最常见的Override
注解的声明
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
注解声明的修饰符,可以是 private
,public
, protected
或者默认 default
这一点跟定义一个类或者接口相同。
在声明一个注解的时候我们常常需要一些其他注解来修饰和限制该定义注解的使用和运行方式。上述的 @Target
和 @Retention
就是如此,我们称之为元注解,详细的元注解在下边说明。
注解成员
注解跟一个类相似,它们并不是都是像上面的 @Override
一样只有声明。一个类大概可以包含构造函数,成员变量,成员函数等,而一个注解只能包含注解成员,注解成员的声明格式为:
类型 参数名() default 默认值;
注解成员可以是:
基本类型
byte
,short
,int
,long
,float
,double
,boolean
八种基本类型及这些类型的数组, 注意这里没对应基本数据类型的包装类。String
,Enum
,Class
,annotations
及这些类型的数组注解的成员修饰符只能是
public
或默认(default)注解元素必须有确定的值,可以在注解中定义默认值,也可以使用注解时指定。即我们在定义注解的时候声明的成员,可以不赋值,但是就跟抽象函数一样,在使用的时候就必须指定。
如:
public @interface TestAnnotation {
String value() default "";
String[] values();
int id() default -1;
int[] ids();
// 错误的不能使用包装类 以及自定义类型
// Integer idInt();
// Apple apple();
enum Color {BULE, RED, GREEN}
Color testEnum() default Color.BULE;
Color[] testEnums();
//注解类型成员 注解元素必须有确定的值,可以在注解中定义默认值,也可以使用注解时指定
FruitName fruitName() default @FruitName("apple");
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
protected @interface FruitName {
String value();
String alias() default "no alias";
}
元注解
我们在 Override 注解的声明中可以看到还有注解修饰着如@Target(ElementType.METHOD)
,我们讲元注解理解为修饰注解定义的注解。换句话说元注解为 JDK 提供给我们的一些基本注解,我们使用元注解来定义一个注解是如何工作的。
JDK 1.8 中存在的元注解有以下 5 种:
@Target, @Retention、@Documented、@Inherited、@Repeatable
下面我们依次来说明这几种类型的注解是如何使用的。
@Target 元注解
@Target 指定了被修饰的注解运用的地方,这些 "地方" 定义在 ElementType
类中,包括:
ElementType.ANNOTATION_TYPE
可以给一个注解进行注解ElementType.CONSTRUCTOR
可以给构造方法进行注解ElementType.FIELD
可以给属性进行注解ElementType.LOCAL_VARIABLE
可以给局部变量进行注解ElementType.METHOD
可以给方法进行注解ElementType.PACKAGE
可以给一个包进行注解ElementType.PARAMETER
可以给一个方法内的参数进行注解ElementType.TYPE
可以给一个类型进行注解,比如类、接口、枚举
其中 METHOD
、PARAMETER
、FIELD
最为常见,如 Override
注解被 @Target(ElementType.METHOD)
修饰,如果我们想要标记一个参数不能为空则可以使用 @NonNull
去修饰一个 param, FIELD
用来指定注解只能用来修饰成员变量如我们经常使用的 @BindView
。
值得注意的是 @Target 元注解定义如下,
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
ElementType[] value();
}
它内部的成员为ElementType[]
数组也就是说,我们可以同时指定一个注解可以用于很多地方。如 @ColorRes 的注解的元注解为@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE})
。
@Retention 元注解
Retention 翻译过来是保留期的意思。当@Retention
用于修饰一个注解上的时候,它规定了了被修饰的注解应用的时期,或者存活的时期
它可以有如下 3 种取值:
-
RetentionPolicy.SOURCE
注解只在源码阶段保留,在编译器进行编译时它将被丢弃。 -
RetentionPolicy.RUNTIME
注解可以保留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时通过反射获取到它们,并解释他们。 -
RetentionPolicy.CLASS
注解只被保留到编译进行的时候,它并不会被加载到 JVM 中。
源码级别注解 RetentionPolicy.SOURCE
对于第一种 RetentionPolicy.SOURCE
注解只在源码阶段保留,更多的效果时做一些编译检查,在 Android 中有个为 @IntDef
的注解,他可以和常量组合一起 代替枚举 enum 做参数限制作用,来优化内存使用。
这里说只是替代了参数限制作用,而 JDK 1.5 为我们带来的 enum 的作用不只是简单的参数限制作用作用,对于 Enum 更多优雅使用可以参考 《Effective Java》。
如 @IntDef
的注解定义如下:
@Retention(SOURCE)
@Target({ANNOTATION_TYPE})
public @interface IntDef {
long[] value() default {};
boolean flag() default false;
}
如我们常用的设置一个 View 的可见属性就使用了 @IntDef
注解来保证使用者传入的参数是对的,如下:
@IntDef({VISIBLE, INVISIBLE, GONE})
@Retention(RetentionPolicy.SOURCE)
public @interface Visibility {}
@RemotableViewMethod
public void setVisibility(@Visibility int visibility) {
setFlags(visibility, VISIBILITY_MASK);
}
//设置一个 View 的属性:
...
toolbar.setVisibility(View.VISIBLE);// it is Ok
//toolbar.setVisibility(1000);// 如果我们随便写一个数值 那么编辑器将会报错
运行期时的注解 RetentionPolicy.RUNTIME
源码级别的注解对我们的编码约束,运行期注解与之不同的是,如果要是让该注解生效,我们必须要编写一定的代码去将定义好的注解,在运行中"注入"应用中,看到运行时注入就可以应该能想得起反射,是的注入这个操作就是需要开发人员自己编写的。
另外,我们也都了解,在运行反射的时候效率是无法保证的。因为反射将遍历对应类的 Class 文件来获取相应的信息。所以运行时注解,并不是那么广泛被运用,而稍后我们要说明的编译期注解则不会对程序的运行造成效率的影响,因此应用更广泛一些。
我们来试着写一个 Dota 英雄名称的运行期注解来了解下他的运作方式:
/**
* 定义一个注解表示英雄的名字
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
private @interface HeroName {
String value();
String alias();
}
/**
* 定义一个类包含英雄名称的属性
*/
public class Hero {
// 定义注解的时候没有 deflaut 属性名称所以在使用的时候必须赋值
@HeroName(value = "Spirit Walker", alias = "SB")
private String heroName;
public void setHeroName(String heroName) {
this.heroName = heroName;
}
public String getHeroName() {
return heroName;
}
}
ok 声明就是这么简单,那么如何让一个属性生效呢,这时候我们就需要一个注解处理方法。为了方便观察运行注解的结果,所以我们这个处理方法选择传递一个 Hero 对象,不过你为了更通用也可以不用这么做。
/** 运行时注解处理方法*/
public static void getHeroNameInfo(Hero hero) {
try {
Class<? extends Hero> clazz = hero.getClass();
Field field = clazz.getDeclaredField("heroName");
// Field isAnnotationPresent 判断一个属性是否被对应的注解修饰
if (field.isAnnotationPresent(HeroName.class)) {
//field.getAnnotation 获取属性的注解
HeroName fruitNameAnno = field.getAnnotation(HeroName.class);
hero.setHeroName("name = " +fruitNameAnno.value() +" alias = " + fruitNameAnno.alias());
}
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
下面我们来运行下程序测试下:
public static void main(String[] args) {
Hero hero = new Hero();
getHeroNameInfo(hero);
System.out.println("hero = " + hero);
}
运行结果:
hero = Hero{heroName='name = Spirit Walker alias = SB'}
通过上述的例子,可以了解运行时注解就是这样声明和运用的。相信 SB 这个别名更容易让大家记得这个例子(白牛这个英雄其实很好玩,只是别名...)。
编译时期的注解 RetentionPolicy.CLASS
经过运行时注解的了解,相比对于注解应该都有一个大概的了解了。接下来到了编译时注解,这个注解类型,便是众多工具库中应用的注解类型,它不会影响运行时的效率问题,而是在编译期,或者打包过程中就生成了对应的代码,在运行时将会生效。如我们常见的 ButterKnife
和 EventBus
。
编译时注解与运行时注解不同,编译时注解主要是帮助我们在编译器编译期使用注解处理器生成相应的代码,帮我们解放劳动力。
我们知道运行时注解是通过反射来解释对应注解并使注解生效的,那么编译时如何解释对应的注解呢?这里就需要用到注解处理器的知识了。
注解处理器(Annotation Processor)是javac的一个工具,它用来在编译时扫描和处理注解(Annotation)。你可以自定义注解,并注册相应的注解处理器(自定义的注解处理器需继承自AbstractProcessor)。
Java 中提供给我们了注解处理器实现方法,主要是通过实现一个名为 AbstractProcessor
的注解处理器基类。该抽象类要求我们必须实现 process 方法来定义处理逻辑。下边我们来看下注解处理器中的几个方法的作用:
public class NameProcessor extends AbstractProcessor {
//会被注解处理工具调用,并输入ProcessingEnviroment参数。ProcessingEnviroment提供很多有用的工具类如Elements, Types和Filer等
@Override
public synchronized void init(ProcessingEnvironment env){ }
//返回最高所支持的java版本, 如返回 SourceVersion.latestSupported();
@Override
public SourceVersion getSupportedSourceVersion() { }
//一个注解处理器可能会处理多个注解逻辑,这个方法将返回待处理的注解类型集合,返回值作为参数传递给 process 方法。
@Override
public Set<String> getSupportedAnnotationTypes() { }
//process 函数就是我们处理待处理注解的地方了,我们需要在这里编写生成 java 文件的具体逻辑。 方法返回布尔值类型,表示注解是否已经处理完成。一般情况下我们返回 true 即可。
@Override
public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }
}
注解处理器的处理步骤主要有以下:
- 编译器开始执行注解处理器
- process 方法中循环处理注解元素(Element),找到被该注解所修饰的类,方法,或者属性
- 拿上一步得到的备注注解修饰的类或者属性,方法,生成对应的辅助类,并写入 java 文件
- 生成 java 文件后就可以在运行时,在程序中获取并调用对应的辅助方法,如
ButterKnife.bind(this);
方法就是获取对应 Activity 的注解处理器生成的java 文件,并执行了构造函数。
自定义一个编译时注解
自定义编译时注解要比运行时注解要繁琐一些。下面我们来举一个简单的例子,意在说明编译时注解是如何工作的。
在 Android 中为了实现一个编译时注解我们一般需要借助两个三方库:
com.google.auto.service:auto-service:1.0-rc2
这是谷歌官方提供的一个注解处理注册插件可以帮助我们更方便的注册注解处理器,只需要在自定义的 Processor 类上方添加@AutoService(Processor.class)
即可,不用自己动手执行注解处理器的注册工作(即编写 resource/META-INF/services/javax.annotation.processing.Processor文件)。为了更方便的在 process 文件中生成 Java 类,需要依赖一个 Square 公司开源的 javapoet 库,
com.squareup:javapoet:1.9.0
这个库中包装提供了一些好用的 API 帮助我们更快更准确的构建 .java 文件。当然你也可以自己手写拼接字符串然后写入文件(如果你能保证正确)。
仿照 ButterKnife 的实现,我们建立一个新的 Android project ,然后创建两个 Java Moudle,其中 processor
用来存放注解处理器,processor-lib
用来存放对应的注解,如下图所示:
在注解处理器存在的lib的 build.gradle 中添加依赖关系:
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
compile project(':processor-lib')
compile 'com.squareup:javapoet:1.9.0'
compile 'com.google.auto.service:auto-service:1.0-rc3'
}
主 moudle 中也需要添加对 processor 和 processor -lib 的依赖:
dependencies {
....
implementation project(':processor-lib')
// 注意这里的注解处理器的依赖方式
annotationProcessor project(':processor')
}
好了经过上述的准备我们终于能够编写我们的编译时注解了:
- 在 processor-lib 定义一个 Name 注解如下:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface Name {
String name();
String alias();
}
- 编写两个类使用我们定义的注解:
public class SBHero {
@Name(name = "Spirit Walker", alias = "SB")
private String heroName;
}
public class PAHero {
@Name(name = "Phantom Assassin", alias = "PA")
private String heroName;
}
- 在 processor 注解处理lib 下定义一个 NamePorcessor
// @AutoService(Processor.class) 帮助我们生成对应的注解处理器配置
@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("com.wangshijia.www.processor.Name")
public class NamePorcessor extends AbstractProcessor {
//文件写入工具类
private Filer filer;
//可以帮助我们在 gradle 控制台打印信息的类
private Messager messager;
// 元素操作的辅助类
private Elements elementUtils;
//自定义文件名的后缀
private static final String SUFFIX = "AutoGenerate";
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
filer = processingEnv.getFiler();
messager = processingEnv.getMessager();
elementUtils = processingEnv.getElementUtils();
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
/**
* @return 你所需要处理的所有注解,该方法的返回值会被 process()方法所接收, 这里其实只有Name 注解,
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
HashSet<String> set = new HashSet<>();
set.add(Name.class.getCanonicalName());
return set;
}
...
}
-
最后我们要编辑我们的 process 方法了,process 方法中一共进行了下面这几件事:
- 遍历程序中所有被该注解修饰器处理注解修饰的元素 存放进创建的Map集合
- 依次取出map 中的元素构建对应的类和方法
- 构建对应的方法内容
- 生成.java 文件 位置在
~/app/build/generated/source/apt
目录下
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
String packageName= "";
// 获得被该注解声明的元素
Set<? extends Element> elememts = roundEnv.getElementsAnnotatedWith(Name.class);
// 声明一个存放成员变量的列表
List<VariableElement> fields;
//key 对应包含注解修饰元素的类的全类名 vaule 代表所有被注解修饰的变量
Map<String, List<VariableElement>> maps = new HashMap<>();
// 遍历程序中所有被该注解修饰器处理注解修饰的元素
for (Element ele : elememts) {
// ele.getKind() 获取注解修饰的成员的类型,判断该元素是否为成员变量
if (ele.getKind() == ElementKind.FIELD) {
VariableElement varELe = (VariableElement) ele;
// 获取该元素封装类型
TypeElement enclosingElement = (TypeElement) varELe.getEnclosingElement();
// 拿到包含 enclosingElement 元素的类的名称 样式如 com.wangshijia.www.annotationapplication.Hero
String key = enclosingElement.getQualifiedName().toString();
messager.printMessage(Diagnostic.Kind.NOTE, "Printing: " + key);
packageName = elementUtils.getPackageOf(enclosingElement).getQualifiedName().toString();
fields = maps.get(key);
if (fields == null) {
maps.put(key, fields = new ArrayList<>());
}
fields.add(varELe);
}
}
/*
* maps 包含有所有被 @Name 修饰的类
*/
for (String key : maps.keySet()) {
List<VariableElement> elementFileds = maps.get(key);
messager.printMessage(Diagnostic.Kind.NOTE, "Printing: " + key);
messager.printMessage(Diagnostic.Kind.NOTE, "Printing: " + elementFileds);
String className = key.substring(key.lastIndexOf(".") + 1);
className += SUFFIX;
// 创建 className 类
TypeSpec.Builder classBuilder = TypeSpec.classBuilder(className)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL);
// 创建方法
MethodSpec.Builder methodBuild = MethodSpec.methodBuilder("printNameAnnotation")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(void.class);
//创建方法中的打印语句
for (VariableElement e : elementFileds) {
Name annotation = e.getAnnotation(Name.class);
// 创建 printNameAnnotation 方法
methodBuild
.addStatement("$T.out.println($S)", System.class, e.getSimpleName() + " = " + annotation.name())
.addStatement("$T.out.println($S)", System.class, e.getSimpleName() + " = " + annotation.alias());
}
//将方法中添加到类中
MethodSpec printNameMethodSpec = methodBuild.build();
TypeSpec classTypeSpec = classBuilder.addMethod(printNameMethodSpec).build();
try {
//构造的 java 文件 参数一 包名,参数二 上述构建的类描述 TypeSpec
JavaFile javaFile = JavaFile.builder(packageName, classTypeSpec)
.addFileComment(" This codes are generated automatically. Do not modify!")
.build();
javaFile.writeTo(filer);
} catch (IOException exception) {
exception.printStackTrace();
}
}
上述注释写的很详细了,这里希望不熟悉的朋友,自己动手实现下,才能更好的理解是如何构建对应的文件的。生成的文件位于指定目录下:
-
使用我们定义好的注解生成文件
使用注解生成器生成的 java 文件和普通的类没什么区别,通过编译后就放在上述文件夹中,我们可以正常调用我们构造类的方法,
ButterKnife.bind(this)
实际上就是调用生成类的方法的过程。我们是一个简单的 demo 就不这么复杂的调用了。直接在 App 目录下的任意文件调用,如在一个 Activity 中:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
PAHeroAutoGenerate.printNameAnnotation();
SBHeroAutoGenerate.printNameAnnotation();
}
}
对于 JavaPoet 生成 java 文件的过程如果想深入了解的话可以查看该博客:JavaPoet - 优雅地生成代码
Android 预置的注解
日常开发中,注解能够帮助我们写出更好更优秀的代码,为了更好地支持 Android 开发,在已有的 android.annotation 基础上,Google 开发了 android.support.annotation 扩展包,共计50个注解,帮助开发者们写出更优秀的程序,这五十多种注解得以应用场景各不相同,常见的如 @IntDef @ColorInt @Nullable。
对于这些注解的用途这里不再详细说明,感兴趣的可以去查看下一个朋友写的关于 Android 中注解的作用的文章: Android 注解指南
总结
这篇文章写的时候遇到很多的困难,因为本人对于注解之前了解情况和大多数人一样,只停留在很少的使用阶段,在文章的构成方面也是一改再改。但是功夫不负有心人,在查阅了大量的资料后,学习到了很多注解的使用和原理的知识。也发现自己的知识掌握程度已经落下不少,比如鸿洋大神写的 Android 打造编译时注解解析框架 这只是一个开始 这篇文章在15年的时候就有了,想想当时刚毕业,与大神的距离整整拉开了进3年,让我去哭一会。但是个人认为这是件好事。总比一直停留在用上好一些,每次深一步了解,就感觉我跟大神之间的差距少了一些。
参考
- Thinking In java
- Android 打造编译时注解解析框架 这只是一个开始
- Android 注解指南
- JavaPoet - 优雅地生成代码