注解淺析

注解(Annotation)是JDK1.5開始引入的,與類、接口、枚舉是在同一個層次,雖然在平時開發中我們很少直接和它打交道,但是我們卻經常用到它,例如 Java 本身已經為我們提供了幾個常用注解:@Deprecated@Override@SuppressWarnings等,熟悉吧!除此之外,Android 中有名的ButterKnifeEventBusDagger2Retrofit都用到了注解,所以注解的重要性可見一斑!

一、注解的主要作用

  • 代碼格式檢查,方便IDE檢查出錯誤代碼,例如資源類型注解
  • 減少重復的工作,例如ButterKnife,我們也可以用注解做類似的事情,提高開發效率
  • 信息配置,運行時動態處理,獲取信息,可實現類似配置文件的功能

二、注解的定義

如何定義一個注解呢?可以先看看這些已有的注解是如何實現的,我們先以@Override為例:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

可以看到注解是通過@interface關鍵字來定義的,和接口的定義類似,但是又多了@Target()@Retention(),這些是java中的元注解,元注解可以理解為內置的基礎注解,用來限定、說明自定義注解。除了這兩個元注解外,還有三個元注解@Inherited@Repeatable@Documented,后邊會詳細解釋這些元注解的作用!

再看看@SuppressWarnings注解的實現:

@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
    String[] value();
}

@Override相比最大的區別就是注解體中多了String[] value();,它代表注解的屬性!關于注解屬性也會在后邊詳細的介紹!

所以定義注解時,除了使用@interface還需要考慮元注解注解屬性,一個自定義注解的偽碼如下:

@元注解0
@元注解1
@元注解2
public @interface 注解名稱 {
    類型 attr0();
    類型 attr1();
}

三、元注解

@Retention

代表注解的保留策略,即存活時間。可選的策略如下:

  • RetentionPolicy.SOURCE 注解只保留在源碼,在編譯器進行編譯時會被忽略
  • RetentionPolicy.CLASS 注解由編譯器保存在class文件中,但不需要在運行時由VM保留,無法通過反射讀取,這是默認的策略。
  • RetentionPolicy.RUNTIME 注解由編譯器保存在class文件中,并在運行時由VM保留,可以通過反射讀取。

@Target

代表注解可能出現的語法位置,即可以在哪里使用定義的注解,可選的位置如下:

  • ElementType.TYPE 類、接口(包括注解類型)或枚舉聲明
  • ElementType.FIELD 字段聲明
  • ElementType.METHOD 方法聲明
  • ElementType.PARAMETER 方法的參數聲明
  • ElementType.CONSTRUCTOR 類的構造法聲明
  • ElementType.LOCAL_VARIABLE 局部變量聲明
  • ElementType.ANNOTATION_TYPE 注解聲明
  • ElementType.PACKAGE 包聲明
  • ElementType.TYPE_PARAMETER JDK1.8新加的,類型參數聲明
  • ElementType.TYPE_USE JDK1.8新加的,類型使用聲明

通過查看注解的@Target元注解,可以知道注解能在哪些地方使用!

@Inherited

表明注解類型是自動繼承的,怎么理解呢?先看代碼:

@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface InheritedTest {
}
@InheritedTest
public class P {
}
public class P1 extends P {
}

我們自定義了一個被@Inherited注解的InheritedTest注解,同時用自定義的注解來注解P類,如果P類有一個子類P1,則該子類也繼承了父類的InheritedTest注解,有點繞......

@Repeatable

JDK1.8新加的,表明當自定義注解的屬性值有多個時,自定義注解可以多次使用,舉個例子,手機可以有打電話、上網、相機等功能,可把這些功能看做一個個注解,并給手機類使用,使手機類具有對應功能,具體看代碼:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Funcs {
    Func[] value();
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Funcs.class)
public @interface Func {
    String name() default "";
}
@Func(name = "CALL")
@Func(name = "INTERNET")
@Func(name = "CAMERA")
public class XPhone {
}

我們定義注解Func使用了@Repeatable(Funcs.class),其中Funcs也是我們自定義的注解:

public @interface Funcs {
    Func[] value();
}

其中有一個數組類型的 value屬性,而且名稱必須為value,關于注解的屬性后邊會說到。由于@Func可以重復使用,@Funcs就相當于接收重復注解的容器。

如果在AS中無法多次使用@Func,請確認是否配置了JDK1.8:

android {
    ......
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

@Documented

這個相對簡單,說明該注解將被包含在javadoc中。

四、注解的屬性

在注解中定義屬性和在接口中定義方法的格式類似,例如:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
    String name();
    int age() default 18;
    String[] favour();
}

這樣就給Test注解定義了nameage兩個屬性,并用default關鍵字指定age的默認值為18。可以這樣使用定義好的注解:

@TestAnnotation(name = "Tom", age = 12, favour = {"music", "sports"})
public class Test {
}

由于age有默認值,可以在使用注解時不指定它的值。由于favour的類型為數組,所以當其有多個值時需要用{}包起來。
如果自定義注解沒有屬性或者屬性有默認值,則使用時可以直接寫@TestAnnotation,省略后邊的括號。

注解的屬性支持的數據類型如下:

  • 基本類型(byte、short、int、float、double、long、char、boolean),不包括其對應的包裝類型
  • String
  • Class,即Class<?>
  • enum,例如enum staus {A, B, C}
  • 注解,例如Override test();
  • 上述類型對應的數組

注解相關的語法糖就介紹到這里了,接下來要關注的是當一個類、方法、屬性等使用了注解后,如何提取注解上的信息。

五、注解與反射

要提取注解上的信息,就要用到反射相關的知識了,下面看一個完整的例子,首先定義TestAnnotation注解,可以作用在類、字段、方法聲明的地方,并可以在運行時被獲取,以及三個屬性:

@Target(value = {ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
    String name();
    int age() default 18;
    String[] favour();
}

Test的類、字段、方法聲明的地方分別使用TestAnnotation注解:

@TestAnnotation(name = "Test", age = 20, favour = {"music", "sports"})
public class Test {

    @TestAnnotation(name = "testField", favour = {"reading", "sports"})
    private int testField;

    @TestAnnotation(name = "testMethod", age = 10, favour = {"dancing", "music"})
    public void testMethod() {

    }

    @TestAnnotation(name = "testMethod1", age = 12, favour = {"music"})
    public void testMethod1() {

    }
}

通過反射的方式提取注解信息:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        resolve();
    }

    private void resolve() {
        // 解析類上的注解
        boolean isPresent = Test.class.isAnnotationPresent(TestAnnotation.class);
        if (isPresent) {
            TestAnnotation annotation = Test.class.getAnnotation(TestAnnotation.class);
            showAnnotation(annotation);
        }
        // 解析字段上的注解
        Field[] fields = Test.class.getDeclaredFields();
        for (Field field : fields) {
            if (field.isAnnotationPresent(TestAnnotation.class)) {
                TestAnnotation annotation = field.getAnnotation(TestAnnotation.class);
                showAnnotation(annotation);
            }
        }
        // 解析方法上的注解
        Method[] methods = Test.class.getDeclaredMethods();
        for (Method method : methods) {
            if (method.isAnnotationPresent(TestAnnotation.class)) {
                TestAnnotation annotation = method.getAnnotation(TestAnnotation.class);
                showAnnotation(annotation);
            }
        }
    }

    private void showAnnotation(TestAnnotation annotation) {
        Log.e("Annotation", annotation.name() + "#" + annotation.age() + "#" + Arrays.toString(annotation.favour()));
    }
}

運行后的效果如下:


0

其中涉及到了幾個關鍵的方法,Class、Method、Field等類都有這樣的方法:

  • boolean isAnnotationPresent(Class<? extends Annotation> annotation),用來判斷是否使用了某個注解。
  • public <A extends Annotation> A getAnnotation(Class<A> annotation),獲得指定名稱的注解對象。
  • public Annotation[] getAnnotations(),返回對應元素的全部注解。
  • public Annotation[] getDeclaredAnnotations(),返回直接在對應元素上使用的注解,不包括父類的注解。

六、枚舉的替代方案

由于性能、開銷的問題,在Android中不建議直接使用枚舉,要實現類似枚舉的功能可以使用android.support.annotation包提供的@IntDef@StringDef注解可以解決這個問題。
例如要表示一個Button在界面的位置,使用枚舉可以這樣定義:

public enum Position {
    LEFT, TOP, RIGHT, BOTTOM
}

如果使用@IntDef注解,則這樣實現:

@IntDef({LEFT, TOP, RIGHT, BOTTOM})
@Retention(RetentionPolicy.SOURCE)
public @interface Position {
    int LEFT = 0;
    int TOP = 1;
    int RIGHT = 2;
    int BOTTOM = 3;
}

如果使用@StringDef實現也類似,只是對應的位置常量為String類型。

看一個簡單的使用例子,在自定義View中提供一個setButtonPosition()方法,其參數使用了@Position注解:

public class MyView extends RelativeLayout {
    @IntDef({LEFT, TOP, RIGHT, BOTTOM})
    @Retention(RetentionPolicy.SOURCE)
    public @interface Position {
        int LEFT = 0;
        int TOP = 1;
        int RIGHT = 2;
        int BOTTOM = 3;
    }

    public MyView(Context context) {
        this(context, null);
    }

    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    // 設置Button位置
    public void setButtonPosition(@Position int pos) {
        switch (pos) {
            case Position.BOTTOM:
                break;
            case Position.LEFT:
                break;
            case Position.RIGHT:
                break;
            case Position.TOP:
                break;
        }
    }
}

當調用該方法時可以保證傳入的參數是類型安全的,即只能傳入注解中定義的常量:

new MyView(MainActivity.this).setButtonPosition(MyView.Position.BOTTOM);

因為setButtonPosition()方法的參數使用了@Position注解,在AS中可以方便的完善switch-case語句:

1

七、常用的注解

首先在Android中,android.support.annotation包中提供了許多好用的注解,以下列舉部分:

  • 資源類型注解,用來標注元素必須為指定的資源類型,例如@AnimRes@ColorRes@IdRes@LayoutRes@LayoutRes@StringRes
  • 顏色值注解@ColorInt,代表ARGB顏色值,而不是顏色的資源id,注意和@ColorRes區分
  • 空值注解@Nullable:可以為空、@NonNull:不能為空
  • 類型定義注解@IntDef@StringDef
  • 值約束注解,即約束元素取值范圍,@Size@IntRange@FloatRange,例如有如下方法,可以保證設置的month值為[1, 12]
public void setMonth(@IntRange(from = 1, to = 12) int month){
}
  • 權限注解@RequiresPermission,即檢查某操作是否有必要的權限,例如:
@RequiresPermission(Manifest.permission.CAMERA)
public void takePhoto() {
}
  • 線程注解,可以標注方法、構造函數、類、接口、枚舉只能在指定的線程被調用,例如@MainThread@UiThread@WorkerThread
  • 重寫方法注解@CallSuper,當子類重寫父類方法時,要保證父方法也必須被調用,則對應父方法要使用該注解

Java也為我們提供了一些常用的注解:

  • @Override,代表某方法是重寫父類中的方法
  • @Deprecated,表示被標記的內容已經過時、不建議被使用,如果被使用編譯器會報警告,但程序也能正常運行。
  • @SuppressWarnings,由于內容被@Deprecated標記后,編譯器會有警告,如果想忽略警告可以使用@SuppressWarnings
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,106評論 6 542
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,441評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,211評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,736評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,475評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,834評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,829評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,009評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,559評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,306評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,516評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,038評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,728評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,132評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,443評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,249評論 3 399
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,484評論 2 379

推薦閱讀更多精彩內容