注解(Annotation)是JDK1.5開始引入的,與類、接口、枚舉是在同一個層次,雖然在平時開發中我們很少直接和它打交道,但是我們卻經常用到它,例如 Java 本身已經為我們提供了幾個常用注解:@Deprecated
、@Override
、@SuppressWarnings
等,熟悉吧!除此之外,Android 中有名的ButterKnife
、EventBus
、Dagger2
、Retrofit
都用到了注解,所以注解的重要性可見一斑!
一、注解的主要作用
- 代碼格式檢查,方便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
注解定義了name
、age
兩個屬性,并用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()));
}
}
運行后的效果如下:
其中涉及到了幾個關鍵的方法,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
語句:
七、常用的注解
首先在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