Android提高必备——注解(1)

·  阅读 835
Android提高必备——注解(1)

Android提高必备——注解 (1)

Java的注解是一个很有趣的玩意儿,我之前一直觉得它很神奇,但是“神奇”这个词往往是给我们不熟悉、不了解的东西的使用的,当我们基于好奇心去好好探寻一番,也就会成为老熟人了

1. 简介

Java的注解机制是在JDK 5.0的时候进行引入的
可以使用注解的对象涵盖了类、方法、字段、方法参数等配合反射可以获取被标记的内容,从而执行对应的代码逻辑,使得在编译器生成文件时,将对应注解插入字节码,然后JVM保留这些信息,使得在运行时对相应的内容进行获取

2. Java预置的注解

Java代码本身已经提供了几个注解供平常的开发使用,即使自己不刻意去使用应该也会经常看到

image.png

2.1. 常用注解

位于java.lang包下

  • @Override:标记用于检查方法重写
  • @Deprecated:过时方法,通常会另外给出替代方案
  • @SuppressWarnings:镇压警告

这些注解主要用于编译期对代码进行某种意义上的检查

2.2. 元注解

这几种类型位于java.lang.annotation包下,称为元注解的原因主要是它可以用来描述普通的注解的内容,是“注解的注解

3. 注解的结构

应当怎样看懂注解呢?这需要从注解的结构开始分析,这个@XXX究竟是如何发挥作用的呢?

首先来看一个接口Annotation,之前提及的各种注解都是基于该接口实现的

image.png 然后,与之相关的还有RetentionPolicyElementType
这里存在一种对应关系,AnnotationRetentionPolicy属于一一对应的关系,而AnnotationElementType之间则是一对多的关系

了解这几个类之间的对应关系后,我们从类的内容层面了解一下

image.png 可以看到,RetentionPolicy是一个枚举类,包含三个变体
这里简单解释一下三个变体所代表的含义,SOURCE代表注解会被保留到编译期,完成编译便不复存在;CLASS是默认的变体,注解会被保留的程序编译完的字节码文件中,但是不会留到虚拟机运行时;RUNTIME则是能够在前面两者的基础之上更进一步,保留给JVM进行读取,执行反射操作
由此看来,这3个变体代表了一种有效期的概念,SOURCE < CLASS < RUNTIME
对应元注解@Retention的参数

image.png 同样的,ElementType也是一个枚举类,由于变体数量略多一些,直接使用大纲视图浏览一下

image.png 这些变体代表的含义实际上是注解可以修饰的对象,比如说METHOD代表的就是该注解应当修饰一个方法,就是将@XXX写在方法上,相应地,这对应元注解@Target的参数

image.png 依然是拿出熟悉的@Override,看到没,这里声明的便是该注解应当修饰方法,我们的@Override也正是修饰覆写的方法,正好可以印证这一点

4. 举例分析

image.png 拿新的函数式接口的注解举个例子,分析一下各个部分的含义
首先,可以看到@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就派上用场了

image.png 子类对应需要覆写的方法上加上@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();
        }
    }
}
复制代码

一般可以像如上这样使用,附加上替代方案以及原因

image.png

image.png 此时,再调用加上弃用注解的方法就会显示下划线,表明不推荐使用,并且编译会有警告,但是功能并不受影响

5.3. @SuppressWarnings

该注解的作用主要是镇压警告

image.png 比如有这样一块代码,这里明显会产生空指针异常,因此编辑器有警告,但是我一意孤行,就是不想理会,此时,可以用到@SuppressWarnings

image.png 这样就镇压了警告,不会出现黄色的警告代码

并且,我们可以发现,这个注解可以传递参数,其中参数表示镇压的警告等级和范围,对应的参数含义可以自行查阅文档,但是通常在编码过程直接使用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
我们自己的注解处理需要对其进行扩展,随后对四个方法进行覆写,实现我们自己的逻辑

image.png 接下来,我们逐个了解一下

6.2.1. init()

AbstractProcessor中,通过传入的ProcessingEnvironment参数进行初始化

image.png

6.2.2. process()

该方法主要是用于对注解进行具体的处理,根据声明与否进行查询以及后续处理,相当于处理器的main()方法

image.png

6.2.3. getSupportedAnnotationTypes()

返回集合,指定支持的注解类型

image.png

6.2.4. getSupportedSourceVersion()

指定支持的Java版本 image.png

讲这么多,还是要自己写一下,才能有更好的理解,开一个新的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一下,会输出注解处理的日志,此时就拿到了数据

image.png

处理器的模块下的build也有对应的生成文件,这便是@AutoService做的事情

image.png

这就是需要用到的几个模块,分别是注解的JavaLib,主模块,处理器的JavaLib

image.png

总的来说,注解内容还是有很多值得深入了解的地方,目前只能算是混个脸熟,后续还需继续实践学习,增进一下感情

分类:
Android
标签:
收藏成功!
已添加到「」, 点击更改