元数据

什么是元数据

  • 要想理解注解(Annotation)的作用,就要先理解Java中元数据的概念。
  • 元数据是描述数据的数据。
  • 在编程语言上下文中,元数据是添加到程序元素如方法、字段、类和包上的额外信息。对数据进行说明描述的数据。

元数据的作用

  • 一般来说,元数据可以用于创建文档(根据程序元素上的注释创建文档),跟踪代码中的依赖性(可声明方法是重载,依赖父类的方法),执行编译时检查(可声明是否编译期检测),代码分析。
  • 如下:
    • 1)编写文档:通过代码里标识的元数据生成文档
    • 2)代码分析:通过代码里标识的元数据对代码进行分析
    • 3)编译检查:通过代码里标识的元数据让编译器能实现基本的编译检查

Java平台元数据

  • 注解Annotation就是java平台的元数据,是J2SE5.0新增加的功能,该机制允许在Java代码中添加自定义注释,并允许通过反射(reflection),以编程方式访问元数据注释。
  • 通过提供为程序元素(类、方法等)附加额外数据的标准方法,元数据功能具有简化和改进许多应用程序开发领域的潜在能力,其中包括配置管理、框架实现和代码生成。

注解

注解(Annotation)的概念

  • 注解(Annotation)在JDK1.5之后增加的一个新特性,注解的引入意义很大,有很多非常有名的框架,比如Hibernate、Spring等框架中都大量使用注解。注解作为程序的元数据嵌入到程序。注解可以被解析工具或编译工具解析。
  • 关于注解(Annotation)的作用,其实就是上述元数据的作用。
  • 注意:Annotation能被用来为程序元素(类、方法、成员变量等)设置元数据。Annotaion不影响程序代码的执行,无论增加、删除Annotation,代码都始终如一地执行。如果希望让程序中的Annotation起一定的作用,只有通过解析工具或编译工具对Annotation中的信息进行解析和处理。

注解的属性分类

  • 首先注解分为三类:
    • 标准 Annotation
      • 包括 Override, Deprecated, SuppressWarnings,是java自带的几个注解,他们由编译器来识别,不会进行编译, 不影响代码运行,至于他们的含义不是这篇博客的重点,这里不再讲述。
    • 元 Annotation
      • @Retention, @Target, @Inherited, @Documented,它们是用来定义 Annotation 的 Annotation。也就是当我们要自定义注解时,需要使用它们。
    • 自定义 Annotation
      • 根据需要,自定义的Annotation。而自定义的方式,下面我们会讲到。

注解运行时段分类

  • 按照运行的时段,注解可以分为两大类
    • 编译器注解
    • 运行期注解

自定义注解分类

  • 同样,自定义的注解也分为三类,通过元Annotation - @Retention 定义:
    • @Retention(RetentionPolicy.SOURCE)
      • 源码时注解,一般用来作为编译器标记。如Override, Deprecated, SuppressWarnings。
      • 注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;源码注解(RetentionPolicy.SOURCE)的生命周期只存在Java源文件这一阶段,是3种生命周期中最短的注解。当在Java源程序上加了一个注解,这个Java源程序要由javac去编译,javac把java源文件编译成.class文件,在编译成class时会把Java源程序上的源码注解给去掉。需要注意的是,在编译器处理期间源码注解还存在,即注解处理器Processor 也能处理源码注解,编译器处理完之后就没有该注解信息了。
    • @Retention(RetentionPolicy.RUNTIME)
      • 运行时注解,在运行时通过反射去识别的注解。
      • 定义运行时注解,只需要在声明注解时指定@Retention(RetentionPolicy.RUNTIME)即可。
      • 运行时注解一般和反射机制配合使用,相比编译时注解性能比较低,但灵活性好,实现起来比较简答。
    • @Retention(RetentionPolicy.CLASS)
      • 编译时注解,在编译时被识别并处理的注解,这是本章重点。
      • 编译时注解能够自动处理Java源文件并生成更多的源码、配置文件、脚本或其他可能想要生成的东西。

实际注解案例

  • 实际注解案例
    • 运行时注解:retrofit
    • 编译时注解:Dagger2, ButterKnife, EventBus3

Java注解解析

注解的成员变量

  • 注解的属性也叫做成员变量。注解只有成员变量,没有方法

  • 注解的成员变量在注解的定义中以“无形参的方法”形式来声明

    • 其方法名定义了该成员变量的名字
    • 其返回值定义了该成员变量的类型。
    • 在注解中定义属性时它的类型必须是 8 种基本数据类型外加 类、接口、注解及它们的数组。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface TestAnnotation {

    int id();

    String msg();

    }
  • 上面代码定义了 TestAnnotation 这个注解中拥有 id 和 msg 两个属性。在使用的时候,我们应该给它们进行赋值。

  • 赋值的方式是在注解的括号内以 value=”” 形式,多个属性之前用 ,隔开。

    1
    2
    3
    4
    @TestAnnotation(id=3,msg="hello annotation")
    public class Test {

    }
  • 注解中属性可以有默认值,默认值需要用 default 关键值指定。比如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface TestAnnotation {
    // id 属性默认值为 -1
    public int id() default -1;
    // msg 属性默认值为 Hi。
    public String msg() default "Hi";

    }

    //因为有默认值,所以无需要再在 @TestAnnotation 后面的括号里面进行赋值
    @TestAnnotation()
    public class Test {}
  • 如果一个注解内仅仅只有一个名字为 value 的属性时,应用这个注解时可以直接接属性值填写到括号内

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public @interface Check {
    // Check 这个注解只有 value 这个属性
    String value();
    }

    // 所以可以这样应用。
    @Check("hi")
    int a;

    // 这和下面的效果是一样的
    @Check(value="hi")
    int a;
  • 注解没有任何属性。比如

    1
    2
    3
    4
    5
    public @interface Perform {}

    // 应用这个注解的时候,括号都可以省略。
    @Perform
    public void testMethod(){}

案例

  • 创建自定义注解,与创建接口有几分相似,但注解需要以@开头。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @Documented
    @Target(ElementType.METHOD)
    @Inherited
    @Retention(RetentionPolicy.RUNTIME)
    public @interface MyAnnotataion{
    String name();
    String website() default "hello";
    int revision() default 1;
    }
  • 自定义注解中定义成员变量的规则:

    • 其定义是以无形参的方法形式来声明的。
    • 注解方法不带参数,比如name(),website();
    • 注解方法返回值类型:基本类型、String、Enums、Annotation以及前面这些类型的数组类型
    • 注解方法可有默认值,比如default “hello”,默认website=”hello”
  • 当然注解中也可以不存在成员变量,在使用解析注解进行操作时,仅以是否包含该注解来进行操作。当注解中有成员变量时,若没有默认值,需要在使用注解时,指定成员变量的值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class AnnotationDemo {
    @MyAnnotataion(name="lvr", website="hello", revision=1)
    public static void main(String[] args) {
    System.out.println("I am main method");
    }

    @SuppressWarnings({ "unchecked", "deprecation" })
    @MyAnnotataion(name="lvr", website="hello", revision=2)
    public void demo(){
    System.out.println("I am demo method");
    }
    }
  • 由于该注解的保留策略为RetentionPolicy.RUNTIME,故可在运行期通过反射机制来使用,否则无法通过反射机制来获取。这时候注解实现的就是元数据的第二个作用:代码分析

注解常用方法

  • 接下来,通过反射技术来解析自定义注解。

    • 关于反射类位于包java.lang.reflect,其中有一个接口AnnotatedElement,该接口主要有如下几个实现类:Class,Constructor,Field,Method,Package。除此之外,该接口定义了注释相关的几个核心方法,如下:

  • 因此,当获取了某个类的Class对象,然后获取其Field,Method等对象,通过上述4个方法提取其中的注解,然后获得注解的详细信息。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class AnnotationParser {
    public static void main(String[] args) throws SecurityException, ClassNotFoundException {
    String clazz = "com.lvr.annotation.AnnotationDemo";
    Method[] demoMethod = AnnotationParser.class
    .getClassLoader().loadClass(clazz).getMethods();

    for (Method method : demoMethod) {
    if (method.isAnnotationPresent(MyAnnotataion.class)) {
    MyAnnotataion annotationInfo = method.getAnnotation(MyAnnotataion.class);
    System.out.println("method: "+ method);
    System.out.println("name= "+ annotationInfo.name() +
    " , website= "+ annotationInfo.website()
    + " , revision= "+annotationInfo.revision());
    }
    }
    }
    }
    • 以上仅是一个示例,其实可以根据拿到的注解信息做更多有意义的事。

注解的使用

内建注解使用

  • Java提供了多种内建的注解,下面接下几个比较常用的注解:@Override、@Deprecated、@SuppressWarnings以及@FunctionalInterface这4个注解。

    • 内建注解主要实现了元数据的第二个作用:编译检查
  • @Override

    • 用途:用于告知编译器,我们需要覆写超类的当前方法。如果某个方法带有该注解但并没有覆写超类相应的方法,则编译器会生成一条错误信息。如果父类没有这个要覆写的方法,则编译器也会生成一条错误信息。
    • @Override可适用元素为方法,仅仅保留在java源文件中。
  • @Deprecated

    • 用途:使用这个注解,用于告知编译器,某一程序元素(比如方法,成员变量)不建议使用了(即过时了)。

    • 例如:Person类中的info()方法使用@Deprecated表示该方法过时了。

      1
      2
      3
      4
      5
      6
      public class Person {
      @Deprecated
      public void info(){

      }
      }
    • 调用info()方法会编译器会出现警告,告知该方法已过时。

  • @SuppressWarnings

    • 用途:用于告知编译器忽略特定的警告信息,例在泛型中使用原生数据类型,编译器会发出警告,当使用该注解后,则不会发出警告。
    • 注解类型分析: @SuppressWarnings可适合用于除注解类型声明和包名之外的所有元素,仅仅保留在java源文件中。
    • 该注解有方法value(),可支持多个字符串参数,用户指定忽略哪种警告,例如:
    1
    @SupressWarning(value={"uncheck","deprecation"})
  • @FunctionalInterface

    • 用途:用户告知编译器,检查这个接口,保证该接口是函数式接口,即只能包含一个抽象方法,否则就会编译出错。
    • 注解类型分析: @FunctionalInterface可适合用于注解类型声明,保留时长为运行时

元Annotation使用

  • JDK除了在java.lang提供了上述内建注解外,还在java.lang。annotation包下提供了6个Meta Annotation(元Annotataion),其中有5个元Annotation都用于修饰其他的Annotation定义。其中@Repeatable专门用户定义Java 8 新增的可重复注解。

  • 先介绍其中4个常用的修饰其他Annotation的元Annotation。在此之前,我们先了解如何自定义Annotation。

  • 当一个接口直接继承java.lang.annotation.Annotation接口时,仍是接口,而并非注解。要想自定义注解类型,只能通过@interface关键字的方式,其实通过该方式会隐含地继承.Annotation接口。

  • @Documented

    • @Documented用户指定被该元Annotation修饰的Annotation类将会被javadoc工具提取成文档,如果定义Annotation类时使用了@Documented修饰,则所有使用该Annotation修饰的程序元素的API文档中将会包含该Annotation说明。例如:
    1
    2
    3
    4
    5
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
    public @interface Deprecated {
    }
    • 定义@Deprecated时使用了@Documented,则任何元素使用@Deprecated修饰时,在生成API文档时,将会包含@Deprecated的说明
    • 以下是String的一个过时的构造方法:该注解实现了元数据的第一个功能:编写文档
    1
    2
    @Deprecated
    public String(byte[] ascii,int hibyte,int offset, int count)
  • @Inherited

    • @Inherited指定被它修饰的Annotation将具有继承性——如果某个类使用了@Xxx注解(定义该Annotation时使用了@Inherited修饰)修饰,则其子类将自动被@Xxx修饰。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      //当你的注解定义到类A上,此时,有个B类继承A,且没使用该注解。
      //但是扫描的时候,会把A类设置的注解,扫描到B类上。

      @Inherited
      @Retention(RetentionPolicy.CLASS)
      @Target(ElementType.TYPE)
      public @interface Test {
      //...
      }
  • @Retention

    • @Retention:表示该注解类型的注解保留的时长。当注解类型声明中没有@Retention元注解,则默认保留策略为RetentionPolicy.CLASS。关于保留策略(RetentionPolicy)是枚举类型,共定义3种保留策略,如下表:

      策略 保留在.class文件 描述 场景
      RetentionPolicy.SOURCE 一般用于在编译时,通过注解对即将生成的.class文件进行干预,以生成自身期望生成的.class文件的样子 Lombok的@Data注解,会生成成员变量的get/set方法。即.class中会包含get/set方法(源代码.java文件中不需要编写get/set方法)
      RetentionPolicy.CLASS .class文件是用于生成对象的基础,当存在注解时,通过相应的逻辑干预,生成与原生.class文件不同的对象。 编织代理对象。
      RetentionPolicy.RUNTIME 在运行环境VM中保留该注解,可以通过反射读取这些信息。 在运行时保留注解可以在执行时,通过判断方法等是否含有指定的注解,从而在运行时干预方法的执行逻辑,执行期望的过程。如动态代理。
  • @Target

    • @Target:表示该注解类型的所适用的程序元素类型。当注解类型声明中没有@Target元注解,则默认为可适用所有的程序元素。如果存在指定的@Target元注解,则编译器强制实施相应的使用限制。关于程序元素(ElementType)是枚举类型,共定义8种程序元素,如下表:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      @Target(ElementType.TYPE)
      接口、类、枚举、注解
      @Target(ElementType.FIELD)
      字段、枚举的常量
      @Target(ElementType.METHOD)
      方法
      @Target(ElementType.PARAMETER)
      方法参数
      @Target(ElementType.CONSTRUCTOR)
      构造函数
      @Target(ElementType.LOCAL_VARIABLE)
      局部变量
      @Target(ElementType.ANNOTATION_TYPE)
      注解
      @Target(ElementType.package)


      // 查看该类可知是一个枚举
      public enum ElementType {
      TYPE,
      FIELD,
      METHOD,
      PARAMETER,
      CONSTRUCTOR,
      LOCAL_VARIABLE,
      ANNOTATION_TYPE,
      PACKAGE,
      TYPE_PARAMETER,
      TYPE_USE;

      private ElementType() {
      }
      }

注解的默认值

  • 注解可以设置默认值,有默认值的参数可以不写。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Retention(RetentionPolicy.CLASS)
    @Target(ElementType.TYPE)
    public @interface TestAni {
    int id(); //注解参数
    String name() default "default";
    }

    //使用
    @TestAni(id = 1) //name有默认值可以不写
    class Test{
    }

注解的继承

  • 这里讲的继承并不是通过@Inherited修饰的注解。这个“继承”是一个注解的使用技巧,使用上的感觉类似于依赖倒置,来自于ButterKnife源码。

    • 这是ButterKnife的OnClick 注解。特殊的地方在于**@OnClick修饰了注解@ListenerClass**,并且设置了一些只属于@OnClick的属性。
    • 那这样的作用是什么呢?凡是修饰了@OnClick的地方,也就自动修饰了@ListenerClass。类似于@OnClick是@ListenerClass的子类。而ButterKnife有很多的监听注解@OnItemClick、@OnLongClick等等。这样在做代码生成时,不需要再单独考虑每一个监听注解,只需要处理@ListenerClass就OK。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Target(METHOD)
    @Retention(CLASS)
    @ListenerClass(
    targetType = "android.view.View",
    setter = "setOnClickListener",
    type = "butterknife.internal.DebouncingOnClickListener",
    method = @ListenerMethod(
    name = "doClick",
    parameters = "android.view.View"
    )
    )
    public @interface OnClick {
    /** View IDs to which the method will be bound. */
    int[] value() default { View.NO_ID };
    }

自定义注解

  • 看自定义注解部分内容代码,思考下面问题……

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //@Retention用来修饰这是一个什么类型的注解。这里表示该注解是一个编译时注解。
    @Retention(RetentionPolicy.CLASS)
    //@Target用来表示这个注解可以使用在哪些地方。
    // 比如:类、方法、属性、接口等等。这里ElementType.METHOD 表示这个注解可以用来修饰:方法
    @Target(ElementType.METHOD)
    //这里的interface并不是说OnceClick是一个接口。就像申明类用关键字class。申明注解用的就是@interface。
    public @interface OnceClick {
    //返回值表示这个注解里可以存放什么类型值
    int value();
    }
  • Annotation里面的方法为何不能是private?

    • 只能用public或默认(default)这两个访问权修饰.例如,String value();不能是private;因为它是提供给外部使用的。

  • 参数只能使用基本类型byte,short,char,int,long,float,double,boolean八种基本数据类型和 String,Enum,Class,annotations等数据类型,以及这一些类型的数组.例如,String value();这里的参数类型就为String;

编译器注解应用

使用注解替代枚举

  • 代码如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    /**
    * 播放模式
    * -1 播放错误
    * 0 播放未开始
    * 1 播放准备中
    * 2 播放准备就绪
    * 3 正在播放
    * 4 暂停播放
    * 5 正在缓冲(播放器正在播放时,缓冲区数据不足,进行缓冲,缓冲区数据足够后恢复播放)
    * 6 正在缓冲(播放器正在播放时,缓冲区数据不足,进行缓冲,此时暂停播放器,继续缓冲,缓冲区数据足够后恢复暂停
    * 7 播放完成
    */
    public @interface CurrentState{
    int STATE_ERROR = -1;
    int STATE_IDLE = 0;
    int STATE_PREPARING = 1;
    int STATE_PREPARED = 2;
    int STATE_PLAYING = 3;
    int STATE_PAUSED = 4;
    int STATE_BUFFERING_PLAYING = 5;
    int STATE_BUFFERING_PAUSED = 6;
    int STATE_COMPLETED = 7;
    }

运行期注解应用

创建一个注解

  • 如下所示

    1
    2
    3
    4
    5
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE})
    public @interface ContentView {
    int value();
    }
  • 关于代码解释

    • 第一行:@Retention(RetentionPolicy.RUNTIME)
      • @Retention用来修饰这是一个什么类型的注解。这里表示该注解是一个运行时注解。这样APT就知道啥时候处理这个注解了。
    • 第二行:@Target({ElementType.TYPE})
      • @Target用来表示这个注解可以使用在哪些地方。比如:类、方法、属性、接口等等。这里ElementType.TYPE 表示这个注解可以用来修饰:Class, interface or enum declaration。当你用ContentView修饰一个方法时,编译器会提示错误。
    • 第三行:public @interface ContentView
      • 这里的interface并不是说ContentView是一个接口。就像申明类用关键字class。申明枚举用enum。申明注解用的就是@interface。(值得注意的是:在ElementType的分类中,class、interface、Annotation、enum同属一类为Type,并且从官方注解来看,似乎interface是包含@interface的)
      • /*_ Class, interface (including annotation type), or enum declaration _/
      • TYPE,
    • 第四行:int value();
      • 返回值表示这个注解里可以存放什么类型值。比如我们是这样使用的
      • @ContentView(R.layout.activity_home)
      • R.layout.activity_home实质是一个int型id,如果这样用就会报错:
      • @ContentView(“string”)

BaseActivity注解解析

  • 注解的解析就在BaseActivity中。来看一下BaseActivity代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    public class BaseActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //注解解析
    //遍历所有的子类
    for (Class c = this.getClass(); c != Context.class; c = c.getSuperclass()) {
    assert c != null;
    //找到修饰了注解ContentView的类
    ContentView annotation = (ContentView) c.getAnnotation(ContentView.class);
    if (annotation != null) {
    try {
    //获取ContentView的属性值
    int value = annotation.value();
    //调用setContentView方法设置view
    this.setContentView(value);
    } catch (RuntimeException e) {
    e.printStackTrace();
    }
    return;
    }
    }
    }
    }

2.3 实际运用案例

  • 注解申明好了,但具体是怎么识别这个注解并使用的呢?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @ContentView(R.layout.activity_test_video)
    public class TestActivity extends BaseActivity {

    //@ContentView(R.layout.activity_test_video) 这种使用是错误的
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    TextView tv_video = findViewById(R.id.tv_video);
    tv_video.setOnClickListener(v -> startActivity(
    new Intent(TestActivity.this,VideoActivity.class)));
    }

    }
  • 总结一下

    • 这是一个很简单的案例。现在对运行时注解的使用一定有了一些理解了。也知道了运行时注解被人呕病的地方在哪。你可能会觉得*setContentView(R.layout.activity_home)和@ContentView(R.layout.activity_home)*没什么区别,用了注解反而还增加了性能问题。

注解代替枚举

  • 在做内存优化时,推荐使用注解代替枚举,因为枚举占用的内存更高,如何说明枚举占用内存高呢?这是为什么呢?

案例

  • 枚举案例代码

    1
    2
    3
    4
    5
    public enum Numbers {
    One,
    Two,
    Three
    }
  • 使用枚举的场景

    • 分组常量场景:归属于同一分组的常量,比如性别只有男和女,一周只有7天之类的

枚举案例说明

javac编译Numbers后生成字节码,想看看到底Numbers.class到底有什么。因为字节码比较晦涩难懂,还是想办法用jad反编译成Java吧,链接: http://www.javadecompilers.com/jad

反编译成Java

  • 打开反编译生成的Numbers.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    public final class Numbers extends Enum {

    public static Numbers[] values() {
    return (Numbers[])$VALUES.clone();
    }

    public static Numbers valueOf(String name) {
    return (Numbers)Enum.valueOf(com/ycbjie/test/Numbers, name);
    }

    private Numbers(String s, int i) {
    super(s, i);
    }

    public static final Numbers One;
    public static final Numbers Two;
    public static final Numbers Three;
    private static final Numbers $VALUES[];

    static {
    One = new Numbers("One", 0);
    Two = new Numbers("Two", 1);
    Three = new Numbers("Three", 2);
    $VALUES = (new Numbers[] {
    One, Two, Three
    });
    }
    }

使用枚举得出结论

结论

  • 从上面得到如下结论
    • 枚举类是继承于java.lang.Enum的类。
    • 枚举的构造函数是私有的, 防止new出对象。
    • 枚举值是类对象, 且是静态常量(被static final修饰)。
    • 静态代码块内实例化枚举值,由于静态代码块的语法特性,该代码块只执行一次;
    • 默认值0、1、2是在编译时生成的。
    • 枚举类比常量更占内存, 因为一个Java对象至少占16个字节, 而Numbers包含了3个Java对象;而使用3个整型替换的话,只占用4 * 3即12个字节。

枚举损耗性能

  • 先说结论

    • 每一个枚举值都是一个对象,在使用它时会增加额外的内存消耗,所以枚举相比与 Integer 和 String 会占用更多的内存。
    • 较多的使用 Enum 会增加 DEX 文件的大小,会造成运行时更多的开销,使我们的应用需要更多的空间。
    • 如果你的应用使用很多的 Enum ,最好使用Integer 或 String 替代他们,但是这样还会有问题。
  • 如何佐证,第一种比较文件大小

    • 第一步,写一个Numbers3类,反编译,然后看一下class文件的大小。大小是443个字节。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      //代码
      public class Numbers3 {
      private void number(int number){
      switch (number){
      case 1:

      break;
      case 2:

      break;
      case 3:

      break;
      }
      }
      }
    • 第二步,写一个Numbers2类,使用枚举,反编译,然后看一下class文件的大小。大小是743个字节。可以得知,添加枚举后,大小增加了300个字节。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      public class Numbers2 {

      public enum NumbersInt {
      One,
      Two,
      Three
      }

      private void number(NumbersInt number){
      switch (number){
      case One:

      break;
      case Two:

      break;
      case Three:

      break;
      }
      }

      }
    • 第三步,写一个Numbers4类,使用静态常量,反编译,然后看一下class文件的大小。大小是542个字节。可以得知,添加枚举后,大小增加了99个字节。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      public class Numbers4 {
      public static final int ONE = 1;
      public static final int TWO = 2;
      public static final int THREE = 3;

      private void number(int number){
      switch (number){
      case ONE:

      break;
      case TWO:

      break;
      case THREE:

      break;
      }
      }
      }
    • 由此可知,使用枚举的大小增长量是使用static int的3倍。

  • 如何佐证,第二种,比较占用内存的大小[摘自网络]

    • 有这样一份代码,编译之后的dex大小是2556bytes,在此基础之上,添加一些如下代码,这些代码使用普通static常量相关作为判断值:

      • 增加下面那段代码之后,编译成dex的大小是2680 bytes,相比起之前的2556 bytes只增加124 bytes。
    • 假如换做使用enum,情况如下:
      使用enum之后的dex大小是4188 bytes,相比起2556增加了1632 bytes,增长量是使用static int的13倍。

反编译枚举

  • 对Color类进行反编译,进入项目的目录,打开dos命令行,开始执行 javap -c NumbersInt.class

    • NumbersInt类代码
    1
    2
    3
    4
    5
    public enum NumbersInt {
    One,
    Two,
    Three
    }
  • 最终结果如下所示

    • 本来是可以使用几个静态常量代替的NumbersInt类做了这么多额外的操作,分配了这么多内存,这也是Enum在Android不被建议使用的原因。
  • 代码如下所示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    public final class com.ycbjie.ycapt.NumbersInt extends java.lang.Enum<com.ycbjie.ycapt.NumbersInt> {
    public static final com.ycbjie.ycapt.NumbersInt One;

    public static final com.ycbjie.ycapt.NumbersInt Two;

    public static final com.ycbjie.ycapt.NumbersInt Three;

    public static com.ycbjie.ycapt.NumbersInt[] values();
    Code:
    0: getstatic #1 // Field $VALUES:[Lcom/ycbjie/ycapt/NumbersInt;
    3: invokevirtual #2 // Method "[Lcom/ycbjie/ycapt/NumbersInt;".clone:()Ljava/lang/Object;
    6: checkcast #3 // class "[Lcom/ycbjie/ycapt/NumbersInt;"
    9: areturn

    public static com.ycbjie.ycapt.NumbersInt valueOf(java.lang.String);
    Code:
    0: ldc #4 // class com/ycbjie/ycapt/NumbersInt
    2: aload_0
    3: invokestatic #5 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
    6: checkcast #4 // class com/ycbjie/ycapt/NumbersInt
    9: areturn

    static {};
    Code:
    0: new #4 // class com/ycbjie/ycapt/NumbersInt
    3: dup
    4: ldc #7 // String One
    6: iconst_0
    7: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
    10: putstatic #9 // Field One:Lcom/ycbjie/ycapt/NumbersInt;
    13: new #4 // class com/ycbjie/ycapt/NumbersInt
    16: dup
    17: ldc #10 // String Two
    19: iconst_1
    20: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
    23: putstatic #11 // Field Two:Lcom/ycbjie/ycapt/NumbersInt;
    26: new #4 // class com/ycbjie/ycapt/NumbersInt
    29: dup
    30: ldc #12 // String Three
    32: iconst_2
    33: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
    36: putstatic #13 // Field Three:Lcom/ycbjie/ycapt/NumbersInt;
    39: iconst_3
    40: anewarray #4 // class com/ycbjie/ycapt/NumbersInt
    43: dup
    44: iconst_0
    45: getstatic #9 // Field One:Lcom/ycbjie/ycapt/NumbersInt;
    48: aastore
    49: dup
    50: iconst_1
    51: getstatic #11 // Field Two:Lcom/ycbjie/ycapt/NumbersInt;
    54: aastore
    55: dup
    56: iconst_2
    57: getstatic #13 // Field Three:Lcom/ycbjie/ycapt/NumbersInt;
    60: aastore
    61: putstatic #1 // Field $VALUES:[Lcom/ycbjie/ycapt/NumbersInt;
    64: return
    }

注解与反射

  • 需要注意的是,如果一个注解要在运行时被成功提取,那么 @Retention(RetentionPolicy.RUNTIME) 是必须的。

获取类上的注解

  • 注解通过反射获取。首先可以通过 Class 对象的isAnnotationPresent()方法判断它是否应用了某个注解

    1
    2
    public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {}

  • 然后通过 getAnnotation() 方法来获取 Annotation 对象。返回指定类型的注解

    1
    public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {}
  • 或者是 getAnnotations() 方法。返回注解到这个元素上的所有注解

    1
    public Annotation[] getAnnotations() {}1212
  • 如果获取到的 Annotation 如果不为 null,则就可以调用它们的属性方法了。比如

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    @TestAnnotation()
    public class Test {

    public static void main(String[] args) {

    boolean hasAnnotation = Test.class.isAnnotationPresent(TestAnnotation.class);

    if ( hasAnnotation ) {
    TestAnnotation testAnnotation = Test.class.getAnnotation(TestAnnotation.class);

    System.out.println("id:"+testAnnotation.id());
    System.out.println("msg:"+testAnnotation.msg());
    }

    }

    }
    //
    程序的运行结果正是 TestAnnotation 中 id 和 msg 的默认值。 :
    id:-1
    msg:

获取属性、方法上的注解

  • 代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    @TestAnnotation(msg="hello")
    public class Test {

    @Check(value="hi")
    int a;


    @Perform
    public void testMethod(){}


    @SuppressWarnings("deprecation")
    public void test1(){
    Hero hero = new Hero();
    hero.say();
    hero.speak();
    }


    public static void main(String[] args) {

    boolean hasAnnotation = Test.class.isAnnotationPresent(TestAnnotation.class);

    if ( hasAnnotation ) {
    TestAnnotation testAnnotation = Test.class.getAnnotation(TestAnnotation.class);
    //获取类的注解
    System.out.println("id:"+testAnnotation.id());
    System.out.println("msg:"+testAnnotation.msg());
    }


    try {
    Field a = Test.class.getDeclaredField("a");
    a.setAccessible(true);
    //获取一个成员变量上的注解
    Check check = a.getAnnotation(Check.class);

    if ( check != null ) {
    System.out.println("check value:"+check.value());
    }

    Method testMethod = Test.class.getDeclaredMethod("testMethod");

    if ( testMethod != null ) {
    // 获取方法中的注解
    Annotation[] ans = testMethod.getAnnotations();
    for( int i = 0;i < ans.length;i++) {
    System.out.println("method testMethod annotation:"+ans[i].annotationType().getSimpleName());
    }
    }
    } catch (NoSuchFieldException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
    System.out.println(e.getMessage());
    } catch (SecurityException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
    System.out.println(e.getMessage());
    } catch (NoSuchMethodException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
    System.out.println(e.getMessage());
    }

    }

    }
    • 它们的结果如下:

      1
      2
      3
      4
      id:-1
      msg:hello
      check value:hi
      method testMethod annotation:Perform

自定义注解示例

  • 我要写一个测试框架,测试程序员的代码有无明显的异常。

    程序员 A : 我写了一个类,它的名字叫做 NoBug,因为它所有的方法都没有错误。
    我:自信是好事,不过为了防止意外,让我测试一下如何?
    程序员 A: 怎么测试?
    我:把你写的代码的方法都加上 @Jiecha 这个注解就好了。
    程序员 A: 好的。

NoBug.java

  • 代码中,有些方法上面运用了 @Jiecha 注解。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    package ceshi;
    import ceshi.Jiecha;


    public class NoBug {

    @Jiecha
    public void suanShu(){
    System.out.println("1234567890");
    }
    @Jiecha
    public void jiafa(){
    System.out.println("1+1="+1+1);
    }
    @Jiecha
    public void jiefa(){
    System.out.println("1-1="+(1-1));
    }
    @Jiecha
    public void chengfa(){
    System.out.println("3 x 5="+ 3*5);
    }
    @Jiecha
    public void chufa(){
    System.out.println("6 / 0="+ 6 / 0);
    }

    public void ziwojieshao(){
    System.out.println("我写的程序没有 bug!");
    }
    }
  • 定义注解

    1
    2
    3
    4
    5
    6
    7
    8
    9
    package ceshi;

    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;

    @Retention(RetentionPolicy.RUNTIME)
    public @interface Jiecha {

    }
  • 然后,再编写一个测试类 TestTool 就可以测试 NoBug 相应的方法了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    package ceshi;

    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;

    public class TestTool {

    public static void main(String[] args) {
    // TODO Auto-generated method stub

    NoBug testobj = new NoBug();

    Class clazz = testobj.getClass();

    Method[] method = clazz.getDeclaredMethods();
    //用来记录测试产生的 log 信息
    StringBuilder log = new StringBuilder();
    // 记录异常的次数
    int errornum = 0;

    for ( Method m: method ) {
    // 只有被 @Jiecha 标注过的方法才进行测试
    if ( m.isAnnotationPresent( Jiecha.class )) {
    try {
    m.setAccessible(true);
    m.invoke(testobj, null);

    } catch (Exception e) {
    // TODO Auto-generated catch block
    //e.printStackTrace();
    errornum++;
    log.append(m.getName());
    log.append(" ");
    log.append("has error:");
    log.append("\n\r caused by ");
    //记录测试过程中,发生的异常的名称
    log.append(e.getCause().getClass().getSimpleName());
    log.append("\n\r");
    //记录测试过程中,发生的异常的具体信息
    log.append(e.getCause().getMessage());
    log.append("\n\r");
    }
    }
    }


    log.append(clazz.getSimpleName());
    log.append(" has ");
    log.append(errornum);
    log.append(" error.");

    // 生成测试报告
    System.out.println(log.toString());

    }

    }
  • 测试的结果是:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    1234567890
    1+1=11
    1-1=0
    3 x 5=15
    chufa has error:

    caused by ArithmeticException

    / by zero

    NoBug has 1 error.
  • 这样,通过注解我完成了我自己的目的,那就是对别人的代码进行测试。 所以,再问我注解什么时候用?我只能告诉你,这取决于你想利用它干什么用。

注解应用实例

  • 注解运用的地方太多了,因为我是 Android 开发者,所以我接触到的具体例子有下:

JUnit

  • JUnit 这个是一个测试框架,典型使用方法如下:

    • @Test 标记了要进行测试的方法 addition_isCorrect().
    1
    2
    3
    4
    5
    6
    public class ExampleUnitTest {
    @Test
    public void addition_isCorrect() throws Exception {
    assertEquals(4, 2 + 2);
    }
    }

ButterKnife

  • ButterKnife 是 Android 开发中大名鼎鼎的 IOC 框架,它减少了大量重复的代码。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv_test)
    TextView mTv;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    ButterKnife.bind(this);
    }
    }

Retrofit

  • 很牛逼的 Http 网络访问框架

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public interface GitHubService {
    @GET("users/{user}/repos")
    Call<List<Repo>> listRepos(@Path("user") String user);
    }

    Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com/")
    .build();

    GitHubService service = retrofit.create(GitHubService.class);

总结

  1. 如果注解难于理解,你就把它类同于标签,标签为了解释事物,注解为了解释代码。
  2. 注解的基本语法,创建如同接口,但是多了个 @ 符号。
  3. 注解的元注解。
  4. 注解的属性。
  5. 注解主要给编译器及工具类型的软件用的。
  6. 注解的提取需要借助于 Java 的反射技术,反射比较慢,所以注解使用时也需要谨慎计较时间成本。