Android 进阶——灵活借助自定义注解Annotation高效替代枚举常量

引言

自定义注解或许很多人都用过或者听说过,但这篇文章的这种用法或许你比较少见,也是站在巨人(源码)的肩膀上借鉴过来的一种高效用法。在前面的文章Android进阶——关于自定义注解Annotation和注解处理器APT的那一些你应该掌握的造轮子必备知识点中只是针对APT反方面的应用介绍了注解其中一个方面的应用,但不要险隘地陷入为主地认为注解只有这些作用,其他任何知识都是如此。

这篇文章非常简单但是实用,如果已经懂得了注解注解跳到第三章,2分钟就学完了。

一、注解Annotation

1、注解概述

Annotation是Java 5开始引入的特性,它提供了一种安全的类似于注释和Java doc的机制。注解相当于是一种嵌入在程序中的元数据,可以使用注解解析工具或编译器对其进行解析,也可以指定注解在编译期或运行期有效,这些元数据与程序业务逻辑无关,并且是供指定的工具或框架使用的。简而言之,注解本质上就是一种标记,可以在程序代码中的关键节点(类、方法、变量、参数、包)上打上标记,项目在编译时或运行时可以自动扫描源文件检测到这些标记,进而通过注解处理器来回调从而执行一些特殊操作

2、可以使用注解的节点

Annotation可被用在 packagestypes(类、接口、枚举、Annotation类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数本地变量(如循环变量、catch参数)等处。

3、定义注解时使用到的元注解

元注解就是负责标志其他自定义注解的系统内置注解,Java5定义了四个标准的元注解:@Target@Retention@Documented和**@Inherited**。

3.1、@Target用于指定使用该注解的节点

java.lang.annotation包下的@Target用于指定被描述的注解可以用什么节点之上,且 @Target的值只能来自java.lang.annotation.ElementType的枚举类型值

  • ElementType.CONSTRUCTOR——用于指定该注解只能使用在构造方法
  • ElementType.FIELD——用于指定该注解只能使用在域
  • ElementType.LOCAL_VARIABLE——用于指定该注解只能使用在局部变量
  • ElementType.METHOD——用于指定该注解只能使用在方法
  • ElementType.PACKAGE——用于指定该注解只能使用在包且这个注解仅是配置在package-info.java中的,而不能直接在某个类的package代码上面配置
  • ElementType.PARAMETER——用于指定该注解只能使用在参数,
  • ElementType.ANNOTATION_TYPE——用于指定该注解只能使用注解类型

3.2、@Retention用于声明Annotation的生命周期

java.lang.annotation包下的 @Retention用于指定Annotation的生命周期(表示需要在什么级别保存该注解信息)。因为有些Annotation仅需要出现在源代码中,有些一些却被编译到class文件中,还有些需要在运行的时候还一直存在。且@Retention的值也只能来自java.lang.annotation.RetentionPolicy的枚举类型值

  • RetentionPolicy.SOURCE——编译之后抛弃,存活的时间是在源码和编译时编译器处理完注解就没了,即它将被限定在Java源文件中,那么这个注解即不会参与编译也不会在运行期起任何作用,这个注解就和注释是一样的效果,只能被阅读Java文件的人看到。

  • RetentionPolicy.CLASS——保留在编译后Class文件中编译时它将被编译到Class文件中,编译器就可以在编译时根据注解做一些处理动作,但是运行时JVM会忽略它,所以我们在运行期也不能读取到

  • RetentionPolicy.RUNTIME——存在于运行阶段编译器将在运行期的加载阶段把注解加载到Class对象中,可由JVM读入,也可以在运行时候通过反射API来获取到注解的信息,并通过判断是否有这个注解或这个注解中属性的值,从而执行不同的程序代码段

3.3、@Documented和@Inherited

java.lang.annotation包下的**@Documented和@Inherited是两个标记注解,没有成员**,其中@Documented用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被例如javadoc此类的工具文档化。而@Inherited 是一个标记注解,如果一个使用了@Inherited修饰的annotation类型被用于一个class,则这个Annotation将被用于该class的子类。

4、自定义注解

在Java 中实现自定义注解十分简单,使用 @interface 关键字声明(与类、接口、枚举的声明语法基本一致),编译程序会自动 继承java.lang.annotation.Annotation接口和完善其他细节,但注解不能继承其他的注解或接口。

4.1、自定义注解的注解体

注解体内的每一个方法实际上是声明了一个配置参数,而方法的名称就是参数的名称,返回值类型就是参数的类型(其中返回值类型只能是基本类型、Class、String、enum,可以通过default来声明参数的默认值)注解参数的可支持数据类型:所有基本数据类型int,float,boolean,byte,double,char,long,short)、String类型、Class类型、Enum类型、Annotation类型及以上所有类型的数组。

4.2、自定义注解的步骤

  • 使用 关键字 @interface 声明注解
  • 使用元注解指定自定义注解使用的地方、生命周期等信息
  • 实现注解体

注解体的实现与普通Java对象的实现语法略有不同,需要注意以下几点:

  • 只能用public或default两个修饰符(默认使用default修饰)。

  • 参数成员只能用八种基本数据类型和String,Enum,Class,annotations等数据类型,以及这一些类型的数组(比如String value(); 即把方法被default修饰,参数成员类型为String,参数名称为value)。

  • 如果只有一个参数成员,最好把参数名称设为"value",后加小括号;若注解体内一个参数也没有则该注解为标注类型的注解)。

  • 注解体中可以在定义参数时 在小括号后使用default 设置默认值

  • ()不是定义方法参数的地方,也不能在括号中定义任何参数,仅仅只是一个特殊的语法

    //通用注解的一般格式
    @Target(ElementType.xx)
    @Retention(RetentionPolicy.xx)
    public @interface 注解名 {
    	//TODO 定义注解体
    	参数类型 参数名 () default 默认值;
    }
    

二、androidx.annotation.IntDef和androidx.annotation.StringDef

Android中比较常用的两个系统自定义注解,用于限定被注解的变量取值在指定类型\范围内

@Retention(SOURCE)
@Target({ANNOTATION_TYPE})
public @interface StringDef {
    /** Defines the allowed constants for this element */
    String[] value() default {};

    boolean open() default false;
}

从源码中可以看到 @StringDef 自定义注解的主要注解体就是:参数类型为String[] 、参数名为默认值value ,value的默认值为空数组,存在于源文件阶段且应用于注解之上。

@Retention(SOURCE)
@Target({ANNOTATION_TYPE})
public @interface IntDef {
    /** Defines the allowed constants for this element */
    int[] value() default {};

    boolean flag() default false;

    boolean open() default false;
}

@IntDef 的实现思想与@StringDef 类似,究其本质和编译原理中的语法树的生成有关,不便展开。

三、使用自定义注解替代枚举常量

前面文章还介绍了使用接口来替代普通常量类,因为接口中的常量值默认就是final static修饰的。如以下代码所示:定义两个自定义注解 @Constants.Sex 和@Constants.Season:分别用于定义性别和季节的变量,此时可以把这两个注解看成是“枚举”类了。


import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import androidx.annotation.IntDef;
import androidx.annotation.StringDef;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
import static java.lang.annotation.ElementType.PARAMETER;

/**
 * @author : Crazy.Mo
 */
public interface Constants {

    @IntDef({Sex.MALE, Sex.FEMALE})
    @Retention(RetentionPolicy.SOURCE)
    @Target({PARAMETER, FIELD, LOCAL_VARIABLE})
    @interface Sex {
        int MALE=0;
        int FEMALE=1;
    }

    @StringDef({Season.SPRING, Season.SUMMER, Season.FALL, Season.WINTER})
    @Retention(RetentionPolicy.SOURCE)
    @Target({PARAMETER, FIELD, LOCAL_VARIABLE})
    @interface Season {
        String SPRING ="spring";
        String SUMMER ="summer";
        String FALL ="fall";
        String WINTER="winter";
    }
}

应用

    @Override
    protected void onResume() {
        super.onResume();
        // getSex(0);
        getSex(Constants.Sex.MALE);
    }
    public void getSex(@Constants.Sex int type){
        if(type==Constants.Sex.FEMALE){
            Log.e(TAG,"性别为女"+"Constants.Sex.FEMALE ");
        }else if(type==Constants.Sex.MALE){
            Log.e(TAG,"性别为男");
        }
    }

若在getSex方法直接任意整型实参,就会收到lint 警告,但是不影响编译,也不影响运行,因为@IntDef 只存在于Source阶段,编译后就不存在这个限制了

在这里插入图片描述

    public void getSeason(@Constants.Season String season){
        if(season==Constants.Season.SPRING){
            Log.e(TAG,"春季");
        }else if(season==Constants.Season.SUMMER){
            Log.e(TAG,"夏季");
        }else if(season==Constants.Season.FALL){
            Log.e(TAG,"秋季");
        }else if(season==Constants.Season.WINTER){
            Log.e(TAG,"冬季");
        }
    }

正确的使用姿势:
在这里插入图片描述
既不用枚举,也实现了编译期的类型检查,同时还避免了只定义静态常量带来的语义模糊的问题。不过由于@IntDef 和@StringDef 注解只存在于Source阶段,限制比较有限,仅仅是提示了个Lint 警告,运行的时候如果传入的是0或者1 会得到正确的结果。

©️2020 CSDN 皮肤主题: 创作都市 设计师:CSDN官方博客 返回首页