Android提高必备——注解 (1)
Java的注解是一个很有趣的玩意儿,我之前一直觉得它很神奇,但是“神奇”这个词往往是给我们不熟悉、不了解的东西的使用的,当我们基于好奇心去好好探寻一番,也就会成为老熟人了
1. 简介
Java的注解机制是在JDK 5.0的时候进行引入的
可以使用注解的对象涵盖了类、方法、字段、方法参数等,配合反射可以获取被标记的内容,从而执行对应的代码逻辑,使得在编译器生成文件时,将对应注解插入字节码,然后JVM保留这些信息,使得在运行时对相应的内容进行获取
2. Java预置的注解
Java代码本身已经提供了几个注解供平常的开发使用,即使自己不刻意去使用应该也会经常看到
2.1. 常用注解
位于java.lang
包下
- @Override:标记用于检查方法重写
- @Deprecated:过时方法,通常会另外给出替代方案
- @SuppressWarnings:镇压警告
这些注解主要用于编译期对代码进行某种意义上的检查
2.2. 元注解
这几种类型位于java.lang.annotation
包下,称为元注解的原因主要是它可以用来描述普通的注解的内容,是“注解的注解”
3. 注解的结构
应当怎样看懂注解呢?这需要从注解的结构开始分析,这个@XXX
究竟是如何发挥作用的呢?
首先来看一个接口Annotation
,之前提及的各种注解都是基于该接口实现的
然后,与之相关的还有
RetentionPolicy
和ElementType
这里存在一种对应关系,Annotation
与RetentionPolicy
属于一一对应的关系,而Annotation
与ElementType
之间则是一对多的关系
了解这几个类之间的对应关系后,我们从类的内容层面了解一下
可以看到,
RetentionPolicy
是一个枚举类,包含三个变体
这里简单解释一下三个变体所代表的含义,SOURCE
代表注解会被保留到编译期,完成编译便不复存在;CLASS
是默认的变体,注解会被保留的程序编译完的字节码文件中,但是不会留到虚拟机运行时;RUNTIME
则是能够在前面两者的基础之上更进一步,保留给JVM
进行读取,执行反射操作
由此看来,这3个变体代表了一种有效期的概念,SOURCE < CLASS < RUNTIME
,对应元注解@Retention
的参数
同样的,
ElementType
也是一个枚举类,由于变体数量略多一些,直接使用大纲视图浏览一下
这些变体代表的含义实际上是注解可以修饰的对象,比如说
METHOD
代表的就是该注解应当修饰一个方法,就是将@XXX
写在方法上,相应地,这对应元注解@Target
的参数
依然是拿出熟悉的
@Override
,看到没,这里声明的便是该注解应当修饰方法,我们的@Override
也正是修饰覆写的方法,正好可以印证这一点
4. 举例分析
拿新的函数式接口的注解举个例子,分析一下各个部分的含义
首先,可以看到@interface
,有点像接口的定义,但是多了个@
,这表示的就是Annotation
接口,表明当前定义的是一个实现了Annotation
接口的类型,简单点说,就是指这是一个注解
这种接口实现的方式相较于其他类型,较为特殊一点,是由编译器实现,并且该类型不能够继承其他的注解或者实现其他的接口
除此以外,可以看到一个@Documented
,这也是一个注解,不过是元注解,顾名思义,应当与文档有关,作用是将当前修饰的这个注解加入到Javadoc中
@Retention(RetentionPolicy.RUNTIME)
根据之前了解的,表明的是注解的有效期,也就是说,该注解一直会保留到JVM进行读取
@Target(ElementType.TYPE)
定义了注解能够修饰的对象
这样一分析,大致清楚了@FunctionalInterface
使用的一些限制
5. 常用注解的使用场景
前面提到了几种常用的注解,那么接下来就具体聊一聊它们各自的使用场景吧
5.1. @Override
@Override
最重要的作用就是用来检验子类对于父类方法的覆写,同时其本身这个标注也为代码增加了可读性
public abstract class Animal {
void sleep() {
}
void eat() {
}
}
复制代码
比如说,先定义一个抽象类,规定了一个动物能做什么
public class Panda extends Animal {
public void eae() { // 故意拼错,其实不会报错
System.out.println("吃竹子");
}
public void sleep() {
System.out.println("睡10个小时");
}
}
复制代码
然后,熊猫实现该接口,它吃竹子,睡10小时,这是通过覆写来实现它自己的习性
但是,这样直接覆写是很容易抄错的,比如eat()
拼错了,那么其实覆写就失败了,但其实可能这样并不好发现
此时,@Override
就派上用场了
子类对应需要覆写的方法上加上
@Override
,一方面是提醒自己这个方法是覆写的父类的,一方面可以检验覆写是否成功,比如像这里,父类没有对应的方法,立马就报错了
现在的IDE很好用,比如idea,直接使用control + O
就可以调出覆写面板
选择对应的方法,工具会直接为方法加上@Overrride
注解
这样子类覆写了什么一目了然,是不是也增加了代码的可读性
5.2. @Deprecated
在一些框架源码,三方库更新的时候,我们常常能够找到这个注解
这个注解传达的意思就是,该方法已被弃用或者说不推荐使用,同时,一般会给出相应的替代方案
像三方库这种通常会迭代、更新,有些代码可能早期版本存在一些潜在的bug,后期作者有了新的处理方案或优化,但是又希望不直接修改之前的,毕竟之前的可能别人已经在用了,那么他就希望从现在开始往后,推荐使用者使用新的方法,那么就可以使用@Deprecated
标注之前的方法
public static class MyUtil {
@Deprecated
static void download() { // 性能不好,推荐使用download2_0代替
try {
Thread.sleep(2000L);
System.out.println("下载完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
static void download2_0() {
try {
Thread.sleep(2L);
System.out.println("下载完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
复制代码
一般可以像如上这样使用,附加上替代方案以及原因
此时,再调用加上弃用注解的方法就会显示下划线,表明不推荐使用,并且编译会有警告,但是功能并不受影响
5.3. @SuppressWarnings
该注解的作用主要是镇压警告
比如有这样一块代码,这里明显会产生空指针异常,因此编辑器有警告,但是我一意孤行,就是不想理会,此时,可以用到
@SuppressWarnings
这样就镇压了警告,不会出现黄色的警告代码
并且,我们可以发现,这个注解可以传递参数,其中参数表示镇压的警告等级和范围,对应的参数含义可以自行查阅文档,但是通常在编码过程直接使用IDE提供的提示即可指哪打哪
当然,使用该注解的目的不是为了忽视问题,而是在确认警告在自己掌控之下的情况下,不希望编译器反复提醒,显得啰嗦
6. 注解处理器
如果没有注解处理器,那么注解也并不会比注释更有用
6.1. 运行时注解处理器
运行时注解处理器会用到反射机制,通过反射来实现注解需要的功能逻辑
那么,首先我们来设计一个这样的注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Art {
String content() default "爆炸"; // 不写艺术就是爆炸
int level();
}
复制代码
内容非常简单,注意上方的元注解,声明了有效期直到JVM运行时装载类,这样才能够用得上反射,并且声明了该注解的修饰对象为Method
,也就是方法
并且,可以看到该注解声明了两个参数,一个是带有默认值的String
类型参数,一个是int
类型参数,这样就定义了一个简单的运行时注解
接下来,就要用刚刚定义的注解去修饰方法了,这里需要再创建一个类,然后定义一个方法
public class Work {
@Art(content = "京剧", level = 10)
private static void print() {
System.out.println("没有感情的打印机。。。");
}
}
复制代码
print()
在这里就是一个摆设,并不需要去调用,我们直接用@Art
注解修饰一下,传递两个参数
这样,火箭设置好了,就等我们按下按钮了
下一步,我们定义处理注解的方法
/**
* 处理注解
*/
private static void processAnnotation() {
// 获取所在类的所有方法
Method[] methods = Work.class.getDeclaredMethods();
for (Method method : methods) {
// 获取对应的注解类型
Art art = method.getAnnotation(Art.class);
// 通过返回的注解对象取出参数
System.out.println("content=" + art.content() + ", level=" + art.level());
}
}
复制代码
这里便是通过反射机制对注解修饰的对象进行处理,获取注解的参数
public static void main(String[] args) {
processAnnotation();
}
复制代码
调用该方法便完成了对于注解的处理过程
6.2. 编译时注解处理器
首先,依然是定义注解,然后放到一个JavaLib
中
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface Love {
int power() default 100;
}
复制代码
这个注解专注于用爱发电,根据元注解,可以知道其有效期直到编译完成,并且修饰的目标对象为类的字段
接下来,就是这块的重头戏了,这关系到一个抽象类AbstractProcessor
我们自己的注解处理需要对其进行扩展,随后对四个方法进行覆写,实现我们自己的逻辑
接下来,我们逐个了解一下
6.2.1. init()
在AbstractProcessor
中,通过传入的ProcessingEnvironment
参数进行初始化
6.2.2. process()
该方法主要是用于对注解进行具体的处理,根据声明与否进行查询以及后续处理,相当于处理器的main()
方法
6.2.3. getSupportedAnnotationTypes()
返回集合,指定支持的注解类型
6.2.4. getSupportedSourceVersion()
指定支持的Java版本
讲这么多,还是要自己写一下,才能有更好的理解,开一个新的JavaLib
@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
Messager messager;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
messager = processingEnv.getMessager();
messager.printMessage(Diagnostic.Kind.NOTE, "START");
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
messager.printMessage(Diagnostic.Kind.NOTE, "开始PROCESS");
for (Element element : roundEnvironment.getElementsAnnotatedWith(Love.class)) {
if (element.getKind() == ElementKind.FIELD) {
messager.printMessage(Diagnostic.Kind.NOTE, "元素值=" +
element.getAnnotation(Love.class).power() + ", 元素=" + element);
}
}
return true;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> annoSet = new LinkedHashSet<>();
// 将名称放入,目前支持该注解
annoSet.add(Love.class.getCanonicalName());
return annoSet;
}
@Override
public SourceVersion getSupportedSourceVersion() {
// 最新Java支持版本
return SourceVersion.latestSupported();
}
}
复制代码
Messager
对象主要用来在编译的时候打印日志,主要的处理逻辑在process()
方法当中,大概就是去匹配对应的注解类型,再根据FIELD
的修饰对象再次缩小范围,最终找到@Love
,打印相关的信息
另外,类的上方加了@AutoService
注解,这个是谷歌提供的一个注解,用于生成服务文件,映射处理器文件,即MyProcessor
,这样就对应了Processor
接口,提供对应的环境参数
dependencies {
implementation fileTree(includes: ['*.jar'], dir: 'libs')
implementation project(':anno-demo') // 刚刚的注解所在的Lib
// AutoService自动装配配置
implementation "com.google.auto.service:auto-service:1.0-rc6"
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
}
复制代码
当前处理器模块下的build.gradle
文件配置一下,这样处理器这里就告一段落
使用就比较简单了
public class MainActivity extends AppCompatActivity {
@Love(power = 90) // 加到成员上即可
private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
}
}
复制代码
主模块引入注解,并且build.gradle
依赖一下
implementation project(':anno-demo') // 注解
annotationProcessor project(':processor') // 注解处理器
复制代码
最后,build
一下,会输出注解处理的日志,此时就拿到了数据
处理器的模块下的build
也有对应的生成文件,这便是@AutoService
做的事情
这就是需要用到的几个模块,分别是注解的JavaLib,主模块,处理器的JavaLib
总的来说,注解内容还是有很多值得深入了解的地方,目前只能算是混个脸熟,后续还需继续实践学习,增进一下感情