Java注解及其实例应用

1,166 阅读12分钟

注: 本篇文章首发于 2018-08-25 CSDN,当初博客搬家到掘金的时候把这篇文章忘了。最近复习整理文章时才发现。因此对文章做了些修改,重新发布到掘金。

开始之前先给大家推荐一下AndroidNote这个GitHub仓库,这里是我的学习笔记,同时也是我文章初稿的出处。这个仓库中汇总了大量的java进阶和Android进阶知识。是一个比较系统且全面的Android知识库。对于准备面试的同学也是一份不可多得的面试宝典,欢迎大家到GitHub的仓库主页关注。

Java注解在我们项目开发 中是非常常见的。比如经常用到的几种java内置的注解:

@Override,表示当前的方法定义将覆盖超类中的方法。

@Deprecated,表示当前方法即将废弃,不推荐使用。

@SuppressWarnings,表示忽略编译器的警告信息。

对于上面几个注解想必大家都不会陌生。除此之外,我们还经常在一些第三方框架中看到一些自定义注解。比如大名鼎鼎的ButterKnife和Arouter都是基于注解实现的。网上关于注解的文章数不胜数,但是,很多章都是贴下注解的定义,然后解释下几种元注解,扔出一个自定义注解的例子就不了了之了。刚接触注解的时候,看了半天注解相关的文章也没弄懂注解到底有什么用,我想很多读者应该都有和我一样的经历。其实注解往往是需要结合反射来用的,离了反射,注解也就失去了灵魂。那么,本篇文章我们会先来学习一下注解的基础知识,然后通过几个实例来认识注解的具体用途。

一、注解基础知识简介

首先我们来看下维基百科上给注解的定义:

Java注解又称Java标注,是Java语言5.0版本开始支持加入源代码的特殊语法元数据。Java语言中的类、方法、变量、参数和包等都可以被标注。和Javadoc不同,Java标注可以通过反射获取标注内容。在编译器生成类文件时,标注可以被嵌入到字节码中。Java虚拟机可以保留标注内容,在运行时可以获取到标注内容。 当然它也支持自定义Java标注。

从定义中我们可以看出来,注解其实就是一个标记,它可以标记类、方法、变量、参数甚至是包。有了这个标记之后呢,我们就可以通过反射获取到被注解标记的这些类、方法或者变量、参数等。从而根据注解信息去进行一些特殊操作。比如结合反射实现一些特殊处理,或者结合APT(Java编译时注解处理器)来动态来生生代码。

说了这么多,我们还是先来认识一下注解吧。

1.注解的声明

同类(class)与接口(interface)一样,注解( @interface)也是一种定义类型,它是在JDK 5.0中引入的。我们可以通过@interface来声明一个注解:

@Documented
@Inherited
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.PARAMETER)
public @interface MAnnotation {
	string name();
    int age() default 18;
}

如上代码,我们声明了一个自定义注解MAnnotation,可以看到注解的结构与声明一个类或者接口有些类似。同时注解也可以有成员变量。如上代码中,我们为其声明了name和age两个成员,并且为age赋了一个默认值。而注解与类和接口最大的不同之处就是需要声明元注解。也就是上述代码的前四行。那什么是元注解呢?我们接着来看。

2.元注解

元注解可以理解为注解的注解。用来提供对给其他的注解做类型说明的。比如说通过元注解可以指定注解的作用范围或者指定注解保留的时期(编译器、字节码或者运行时)。JDK中提供了如下4个元注解:

@Target @Retention @Inherited @Documented

那么接下来我们来逐个了解一下上述四个元注解的所用

(1)元注解之@Target

@Target用于指定注解可以修饰哪些程序元素,例如指定注解可以修饰类、修饰方法或者修饰参数等。我们来看一下@Target的源码:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    /**
     * Returns an array of the kinds of elements an annotation type
     * can be applied to.
     * @return an array of the kinds of elements an annotation type
     * can be applied to
     */
    ElementType[] value();
}

可以看到@Target包含一个类型为ElementType[ ]的成员变量,有趣的一点是Target自己修饰了自己,并且指定了ElementType为ANNOTATION_TYPE,意味着@Target是用来标记(注解)注解的。ElementType是一个枚举类型,我们来看一下它的所有枚举值:

public enum ElementType {
    /** 指定注解能修饰类、接口或枚举类型 */
    TYPE,

    /** 指定注解能修饰成员变量 */
    FIELD,

    /** 指定注解能修饰方法 */
    METHOD,

    /**指定注解能修饰参数 */
    PARAMETER,

    /** 指定注解能修饰构造器 */
    CONSTRUCTOR,

    /** 指定注解能修饰局部变量 */
    LOCAL_VARIABLE,

    /** 指定注解能修饰注解 */
    ANNOTATION_TYPE,

    /** 指定注解能修饰包 */
    PACKAGE,

    /**
     * 指定注解能够修饰类型参数(1.8新加入)
     *
     * @since 1.8
     */
    TYPE_PARAMETER,

    /**
     * 类型使用声明(1.8新加入)
     *
     * @since 1.8
     */
    TYPE_USE
}

在本章第一节中我们自定义的MAnnotation注解被声明了@Target(ElementType.PARAMETER),那么MAnnotation就只能用来注解参数,如果用来修饰了其它元素编译器则会报错。另外如果一个自定义注解没有声明@Target,那么这个注解可以作用于任意程序元素。

(2)元注解之@Retention

Retention意思有保留、保持的意思,它表示注解存在阶段是保留在源码(编译期),字节码(类加载)还是者运行期(JVM中运行)。在@Retention注解中使用枚举RetentionPolicy来表示注解保留时期,@Retention的源码如下:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE})
public @interface Retention {
    RetentionPolicy value();
}

而@Retention中的成员变量RetentionPolicy 同样也是一个枚举类型,其值有三个,如下:

public enum RetentionPolicy {
	/**
	* 该类型修饰的注解信息只会保留在源码里,源码经过编译后,注解信息会被丢弃,不会保留在编译好的字节码里)
	*/
    SOURCE,
    /**
	* 该类型修饰的注解会保留在源码和字节码中,但不会被加载到虚拟机
	*/
    CLASS,
    /**
	* 该类型修饰的注解会在源码、字节码以及JVM中都有保留
	*/
    RUNTIME;
}

例如,在本章第一节中声明的自定义注解MAnnotation 被@Retention(RetentionPolicy.SOURCE)所修饰,那么这个注解只会存在于源码中。

(3)元注解之@Inherited

@Inherited是一个标记注解,指定注解具有继承性。要注意的是它并不是说注解本身可以继承,而是说如果一个父类被 @Inherited 注解的话,那么如果它的子类没有被任何注解标记的话,那么这个子类就继承了父类的注解。可以看到本章第一节中MAnnotation被@Inherited修饰。那么来看下面的一个例子:

@MAnnotation 
public class ClassA{}

public class ClassB extends ClassA {}

ClassA 被 MAnnotation 注解,ClassB 继承 ClassA,那么此时ClassB也拥有@MAnnotation 注解。

(4)元注解之@Documented

@Documented是一个标记注解,本章第一节中的MAnnotation 使用了@Documented修饰,则在用javadoc命令生成API文档后,所有使用注解MAnnotation 修饰的程序元素,将会包含注解MAnnotation 的说明。

以上提到的四种元注解中,最常用的是@Target注解于@Retention。或许看到这里你仍然觉得一头雾水,仍然不知道这些东西有什么用途。那么实属正常情况。我们会在后边章节中举例说明。

二、注解的实例应用

1.在Android中使用注解替代枚举

我们知道,在Android的View中有一个setVisibility(int )的方法,该方法接受一个int类型,用来设置View的可见性。那么既然是一个int参数,那么理论上应该可以接受任何的int类型,但是当我们尝试在这个方法中传入一个10的时候,编译器却报错了,错误如下图: 在这里插入图片描述 编译器告诉我们,这个参数只能接受View.VISIBLE,View.INVISIBLE以及View.GONE。这是如何实现的呢?其实就是通过自定义注解来实现的。我们看下setVisibility的源码:

public void setVisibility(@Visibility int visibility) {
        setFlags(visibility, VISIBILITY_MASK);
    }

可以看到setVisibility中的参数visibility被一个@Visibility的注解修饰了,而@Visibility注解如下:

    @IntDef({VISIBLE, INVISIBLE, GONE})
    @Retention(RetentionPolicy.SOURCE)
    public @interface Visibility {}

其中@IntDef是Android源码中的一个自定义注解,可以用@IntDef来指定一个数组集合,如果一个注解被@IntDef标记,并指定了数组集合,那么这个注解去标记参数时,这个参数只能接受在@IntDef中所指定的几个参数。而Visibility注解中指定了{VISIBLE, INVISIBLE, GONE}三个参数,因此,当我们在setVisibility中传入这三个以外的其它值时,编译器就会提示错误。当然,这一检查流程是IDE完成的,我们无需关心太多。而我们在平时的开发中也可以用此类方法替代枚举。

2.注解结合反射实现ButterKnife功能

第二个例子,我们来看下注解与反射的结合使用来实现一个与ButterKnife类似功能的实例。

在文章开头我们就提到离开反射的注解是没有灵魂的,正是因为反射才赋予了注解实质的用途。那么接下来,我们用注解+反射来模仿并实现一个简易的ButterKnife的功能。要实现的功能列举如下:

  • 使用注解注入布局文件省去setContentView(ButterKnife中并没有提供此功能)
  • 使用注解省去findViewById
  • 使用注解省去setOnClickListener

(1)定义注解

根据以上需求,我们可以定义三个注解。

① InjectLayout注解用于给Activity注入布局文件的注解,该注解用于Activity类上。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface InjectLayout {
    int value() default -1; // Activity布局文件
}

② BindView 注解用于查找控件ID,该注解作用于成员变量上。

@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.FIELD})
public @interface BindView {
    int value() default -1; // View的id
}

③OnClick 注解给View设置监听事件,该注解作用于方法上

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OnClick {
    int[] value(); // View的Id数组
}

以上三个注解因为都是需要结合反射,因此@Retention都需要声明为RetentionPolicy.RUNTIME。接着,我们把以上三个注解分别应用到Activity的元素上,代码如下:

@InjectLayout(R.layout.activity_main)
public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv_test)
    Button mButton;

    @OnClick({R.id.btn_factory,R.id.tv_test})
    public void onClick(View view) {
       switch (view.getId()) {
            case R.id.tv_test:
                Toast.makeText(this, "通过注解点击了Button", Toast.LENGTH_SHORT).show();
                break;
            defaultbreak;
        }
    }
}

(2)使用反射处理注解信息

上一小节中,由于我们没有对注解做任何的操作,因此,实际上这些注解到现在为止是没有任何作用的。仅仅是为Activity的这些元素打上了一个标记。那么接下来,我们就需要通过反射为注解注入灵魂。

① 反射+InjectLayout注解实现绑定Activity布局文件

定义injectLayout方法并传入Activity参数,然后通过Class的isAnnotationPresent方法判断Activity上是否有injectLayout的注解信息,如果有,则读取注解信息,并调用Activity的setContentView方法为Activity设置布局文件,如下:

public static void injectLayout(Activity activity) {
        Class<?> activityClass = activity.getClass();
        if (activityClass.isAnnotationPresent(InjectLayout.class)) {
            InjectLayout injectLayout = activityClass.getAnnotation(InjectLayout.class);
            activity.setContentView(injectLayout.value());
        }
    }

这样,通过在Activity中调用injectLayout方法就可以完成布局文件的绑定。

② 反射+BindView 注解绑定View 在bindView方法中获取到Activity中的所有成员变量并进行遍历,逐个判断成员遍历上是否有BindView 注解,如果包含该注解,则读取注解中的id,并调用findViewById方法通过反射为View赋值。代码实现如下:

	private static void bindView(Activity activity) {
        Class<?> activityClass = activity.getClass();
        Field[] declaredFields = activityClass.getDeclaredFields();
        for (Field field : declaredFields) {
            if (field.isAnnotationPresent(BindView.class)) {
                BindView bindView = field.getAnnotation(BindView.class);
                try {
                    View view = activity.findViewById(bindView.value());
                    field.setAccessible(true);
                    field.set(activity, view);
                } catch (IllegalAccessException e ) {
                    e.printStackTrace();
                }
            }
        }
    }

③ 反射+OnClick实现点击事件的绑定

首先获取到Activity中的所有方法,并进行遍历判断方法上是否包含OnClick注解,如果包含则读取注解信息,因为OnClick注解中的参数是一个数组,因此得到数组后需要遍历该数组并获取到View,并为view设置点击事件,在点击的回调用通过反射来调用被@OnClick注解的方法。代码如下:

    private static void bindOnClick(final Activity activity) {
        Class<?> cls = activity.getClass();
        Method[] methods = cls.getMethods();
        for (int i = 0; i < methods.length; i++) {
            final Method method = methods[i];
            if (method.isAnnotationPresent(OnClick.class)) {
                OnClick mOnclick = method.getAnnotation(OnClick.class);
                int[] ids = mOnclick.value();
                for (int j = 0; j < ids.length; j++) {
                    final View view = activity.findViewById(ids[j]);
                    if(view==null) continue;
                    view.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            try {
                                method.setAccessible(true);
                                method.invoke(activity, view);
                            } catch (IllegalAccessException e) {
                                e.printStackTrace();
                            } catch (InvocationTargetException e) {
                                e.printStackTrace();
                            }
                        }
                    });
                }
            }
        }
    }

在完成以上操作后,则可以在Activity的onCreate方法中分别调用injectLayout、bindView和bindOnClick来完成绑定。我们来看下运行及起来的效果: 这里写图片描述

效果貌似还不错,实现了与ButterKnife的部分功能,甚至我们还比ButterKnife多了一个注入布局的功能。

本节相关源码点这里

3.注解结合Java编译时注解处理器(APT)

上一节中我们使用注解+反射实现了一个简易的ButterKnife功能。但是,我们知道反射是一个比较消耗性能的操作,并且上述操作中还进行了循环遍历,而这些实现多多少少都会对性能造成一定的影响。因此,ButterKnife的实现并非使用的用反射,而是使用APT(Java编译时注解处理器)来实现的。而APT的实现也是基于注解,但是由于APT的相关知识相对复杂,因此就不在本篇文章中展开讲了。请参看下一篇文章《Java进阶--编译时注解处理器(APT)详解》

三、总结

注解的概念非常简单,但是如果只学习注解的知识,却很难理解注解的作用。而网上很多文章往往只讲解注解的概念,却对注解的使用只字不提。这样其实是误导了很多读者,致使很多人看完之后依然是一头雾水,不理解注解是做什么用的。而本篇通过讲解注解的基本概念以及注解的三个实例应用。相信通过这些内容读者一定会对注解有一个深刻的认识。