Java進階--Java註解及其實例應用

語言: CN / TW / HK

注: 本篇文章首發於 2018-08-25 CSDN,當初博客搬家到掘金的時候把這篇文章忘了。最近複習整理文章時才發現。因此對文章做了些修改,重新發布到掘金。

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和int兩個成員,並且為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參數,然後判斷Activity上是否有injectLayout的註解信息,如果有,則讀取註解信息,並通過反射調用Activity的setContentView方法為Activity設置佈局文件,如下:

private static void injectLayout(Activity activity) {
        Class<?> activityClass = activity.getClass();
        if (activityClass.isAnnotationPresent(InjectLayout.class)) {
            InjectLayout mId = activityClass.getAnnotation(InjectLayout.class);
            int id = mId.value();
            try {
                Method method = activityClass.getMethod("setContentView", int.class);
                method.setAccessible(true);
                method.invoke(activity, id);
            } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    }
複製代碼

這樣,通過在Activity中調用injectLayout方法就可以完成佈局文件的綁定。 ② 反射+BindView 註解綁定View 在bindView方法中獲取到Activity中的所有成員變量並進行遍歷,逐個判斷成員遍歷上是否有BindView 註解,如果包含該註解,則讀取註解中的id,並反射調用findViewById方法為該View賦值。代碼實現如下:

	//  處理@BindView
    private static void bindView(Activity activity) {
        Class<?> activityClass = activity.getClass();
        Field[] declaredFields = activityClass.getDeclaredFields();
        for (Field field : declaredFields) {
            if (field.isAnnotationPresent(BindView.class)) {
                BindView mId = field.getAnnotation(BindView.class);
                int id = mId.value();
                try {
                    Method method = activityClass.getMethod("findViewById", int.class);
                    method.setAccessible(true);
                    Object view = method.invoke(activity, id);
                    field.setAccessible(true);
                    field.set(activity, view);
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                }
            }
        }
    }
複製代碼

③ 反射+OnClick實現點擊事件的綁定

首先獲取到Activity中的所有方法,並進行遍歷判斷方法上是否包含OnClick註解,如果包含則讀取註解信息,因為OnClick註解中的參數是一個數組,因此得到數組後需要遍歷該數組並獲取到View,然後通過反射為View設置點擊事件。代碼如下:

    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)詳解》

三、總結

註解的概念非常簡單,但是如果只學習註解的知識,卻很難理解註解的作用。而網上很多文章往往只講解註解的概念,卻對註解的使用隻字不提。這樣其實是誤導了很多讀者,致使很多人看完之後依然是一頭霧水,不理解註解是做什麼用的。而本篇通過講解註解的基本概念以及註解的三個實例應用。相信通過這些內容讀者一定會對註解有一個深刻的認識。