最近整體研究了一下ButterKnife這個架構,順帶着系統回顧梳理了一遍Java注解相關的知識點。本文大緻整理了一下注解的幾個核心部分,内容參考了很多現有的資料,在參考文獻中做出了說明。希望通過本文能對注解的基本原理做一個簡要的梳理,為後續兩篇文章深入的分析ButterKnife實作原理做好鋪墊。
1.注解概述
(1)定義
注解(Annotation)是Java1.5中引入的一個重大修改之一,為我們在代碼中添加資訊提供了一種形式化的方法,使我們可以在稍後某個時刻非常友善的使用這些資料。
注解在一定程度上是把中繼資料與源代碼結合在一起,而不是儲存在外部文檔中。
注解的含義可以了解為java中的中繼資料。中繼資料是描述資料的資料。如下所示:
5.4
這裡的”app_version”就是描述資料”5.4”的資料,這是在配置檔案中寫的,注解是在源碼中寫的。另外一種開發人員很熟悉的中繼資料是注釋(comment)。注釋用來描述源代碼中的類,域和方法的作用等。
(2)注解與注釋的不同注解會影響代碼的行為,而注釋不會;
編譯器對代碼進行處理時,注釋會被直接删除,而注解可能會被保留在位元組代碼中;
(3)注解的作用格式檢查:告訴編譯器資訊,比如被@Override标記的方法如果不是父類的某個方法,IDE會報錯;
減少配置:運作時動态處理,得到注解資訊,實作代替配置檔案的功能;
減少重複工作:減輕編寫“樣闆”代碼的負擔,比如第三方架構ButterKnife,通過注解@BindView減少對findViewById的調用,類似的還有(Lombok,Dagger,AndroidAnnotation等);
(4)注解是如何工作的
注解僅僅是中繼資料,和業務邏輯無關,是以檢視注解類時,發現裡面沒有任何邏輯處理:
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.FIELD})
public @interface InjectView {
int value();
}
如果注解不包含業務邏輯處理,必然有人來實作這些邏輯。注解的邏輯實作是中繼資料的使用者來處理的,注解僅僅提供它定義的屬性(類/方法/變量/參數/包)的資訊,注解的使用者來讀取這些資訊并實作必要的邏輯。當使用java中的注解時(比如@Override、@Deprecated、@SuppressWarnings)JVM就是使用者,由編譯器來負責處理。如果是程式中自定義的注解,就需要開發者或者第三方庫自己去進行處理了。
(5)注解和配置檔案的差別
通過上面的描述可以發現,其實注解幹的很多事情,通過配置檔案也可以幹,比如為類設定配置屬性;但注解和配置檔案是有很多差別的,在實際程式設計過程中,注解和配置檔案配合使用在工作效率、低耦合、可拓展性方面才會達到權衡。
配置檔案:
a.使用場合
外部依賴的配置,比如build.gradle中的依賴配置;
同一個項目團隊内部達成一緻的時候;
非代碼類的資源檔案(比如圖檔,布局,資料,簽名檔案等);
b.優點
降低耦合,配置集中,容易擴充,比如Android應用多語言支援;
對象之間的關系一目了然,比如strings.xml;
xml配置檔案比注解功能齊全,支援的類型更多,比如drawable、style等;
c.缺點
繁瑣;
類型不安全,比如R.java中的都是資源ID,用TextView的setText方法時傳入int值時無法檢測出該值是否為資源ID,但@StringRes可以;
注解:
a.使用場合
動态配置資訊;
代為實作程式邏輯(比如ButterKnife中的@BindView代替實作findViewById);
代碼格式檢查,比如@Override、@Deprecated、@NonNull、@StringRes等,便于IDE能夠檢查出代碼錯誤;
b.優點
在class檔案中,提高程式的内聚性;
減少重複工作,提升開發效率,比如findViewById;
c.缺點
如果對Annotation修改,需要重新編譯整個工程;
業務類之間的關系不如xml配置那樣一目了然;
程式中過多的Annotation,對于代碼的簡潔度有一定影響;
擴充性較差;
(6)注解類型
很多資料對Java Annotation進行了分類,大部分是分為:Java内置、元注解、自定注解。這裡因為考慮到Android庫提供的注解,本文将其總結歸納為如下幾種類型:
Java标準庫中内置的注解(Built-in Java Annotations)
元注解(Meta-annotations)
Android Support Annotations
自定義注解
2.Java标準庫中内置的注解
Java内置注解;定義在java.lang中,Java1.5中内置了三個:@Deprecated、@Overrride、@SuppressWarnings。java1.7中增加了@SafeVarargs,java1.8中增加了@FunctionsalInterface。詳細介紹如下:
(1)java.lang.Override
用于表示一個方法聲明覆寫父類型中的對應方法。Override注解的作用是避免開發人員沒有正确區分方法重載和覆寫而帶來的錯誤。當需要覆寫一個方法的時候,可以在方法聲明前面加上Override注解。如果這個方法實際上并沒有覆寫父類型中的方法,而是進行了重載,那麼編譯器将産生相應的錯誤資訊。如下所示的代碼:
class User{
public boolean equals(User user){
return true;
}
}
User類的equals方法本意是覆寫Object類中的equals方法,提供自己的對象比較方式的實作,而實際上User類中的equals方法并沒有覆寫Object類的equals方法,而是提供了一個重載的形式。這種錯誤在程式設計過程中很難發現。如果加上了Override注解,那麼編譯器會給出錯誤的資訊,開發人員會意識到這個錯誤,并根據需要進行相應的修改。
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
(2)java.lang.Deprecated
屬于标記注解,不需要設定屬性值;可以對構造方法、變量、方法、包、參數标記,告知使用者和編譯器被标記的内容已不建議被使用,如果被使用,編譯器會報警告,但不會報錯,程式也能正常運作:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.LOCAL_VARIABLE, ElementType.METHOD, ElementType.PACKAGE, ElementType.PARAMETER, ElementType.TYPE})
public @interface Deprecated {
}
(3)java.lang.SuppressWarnings
可以對構造方法、變量、方法、包、參數标記,用于告知編譯器忽略指定的警告,不用在編譯完成後出現警告資訊:
@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.CONSTRUCTOR, ElementType.LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
String[] value();
}
(4)@SafeVarargs(Java SE 7)
作用:Claims to the compiler that the annotation target does nothing potentially unsafe to its varargs argument.。
@SafeVarargs用于指明那些使用可變長度參數的方法和構造器是安全的。這些方法能被傳入長度可變的參數。這些參數可以是泛型的。如果它們是泛型參數,使用@SafeVarargs注釋就可以抑制警告資訊。使用該注解的前提是,開發人員必須確定這個方法的實作中對泛型類型參數的處理不會引發類型安全問題。
使用示例:
@SafeVarargs
public static void displayElements(T... array){
for (T element : array) {
System.out.println(element.getClass().getName() + ":" + element);
}
}
在這裡,方法displayElements的形參是可變長度的泛型類型。
(5)@FunctionalInterface(Java SE 8)
作用:An informative annotation type used to indicate that an interface type declaration is intended to be a functional interface as defined by the Java Language Specification. 幫助編譯器檢查函數式接口的合法性。
3.Java标準庫中的元注解(Meta-annotations)
所謂元注解,就是負責注解其他注解的,主要是描述注解的一些屬性,任何注解都離不開元注解,元注解的使用者是JDK,JDK已經幫助我們實作了這些元注解的邏輯。常見的四個類型是@Documented、@Inherited、@Retention、@Target。在Java 8新增了@Repeatable。具體描述如下:
(1)@Target:
作用:用于描述注解的使用範圍,即被描述的注解可以用在什麼地方(Indicates the contexts in which an annotation type is applicable);
取值:
CONSTRUCTOR:構造器;
FIELD:執行個體;
LOCAL_VARIABLE:局部變量;
METHOD:方法;
PACKAGE:包;
PARAMETER:參數;
TYPE:類、接口(包括注解類型) 或enum聲明;
ElementType 常量在 Target 注解中至多隻能出現一次,如下是非法的:@Target({ElementType.FIELD, ElementType.METHOD, ElementType.FIELD})
(2)@Retention:
作用:表示需要在什麼級别儲存該注解資訊,用于描述注解的生命周期,即被描述的注解在什麼範圍内有效(Indicates how long annotations with the annotated type are to be retained.);
取值:
RetentionPolicy.SOURCE:
The marked annotation is retained only in the source level and is ignored by the compiler.
該注解隻保留到代碼層,編譯器将對其忽略。是以其不會出現在生成的class檔案中;
RetentionPolicy.CLASS:
The marked annotation is retained by the compiler at compile time, but is ignored by the Java Virtual Machine (JVM).
該注解保留在編譯器中,是以能出現在生成的class檔案中,但是被JVM忽略,是以不能在運作時擷取注解内容。
RetentionPolicy.RUNTIME:
The marked annotation is retained by the JVM so it can be used by the runtime environment.
該注解能保留在JVM中,可以在運作時通過反射的方法擷取具體内容。
如果自定義注解時不進行指定,預設為RetentionPolicy.CLASS。
(3)@Documented:
作用:訓示該注解是否預設通過Javadoc或者類似的工具進行文檔化,是一種語義元注解(Indicates that annotations with a type are to be documented by javadoc and similar tools by default)。
取值:它屬于标記注解,沒有成員;
(4)@Inherited:
作用:訓示注解類型被自動繼承。如果在解析注解時發現了該字段,并且在該類中沒有該類型的注解,則對其父類進行查詢。舉個例子,如果一個類中,沒有A注解,但是其父類是有A注解的,并且A注解是被@Inherited注解的(不妨認為保留時态是Runtime),那麼使用反射擷取子類的A注解時,因為擷取不到,是以會去其父類查詢到A注解。使用了@Inherited注解的類,這個注解是可以被用于其子類。
取值:它屬于标記注解,沒有成員;
(5)@Repetable(Java 8中新增加的):
作用:訓示該注解是否可以多次使用(used to indicate that the annotation type whose declaration it (meta-)annotates is repeatable.)。在這個注解出現前,一個位置要想注兩個相同的注解,是不可能的,編譯會出錯誤。是以要想使一個注解可以被注入兩次,需要聲明一個進階注解,這個注解中的成員類型為需要多次注入的注解的注解數組。
取值:它屬于标記注解,沒有成員;
使用示例:
在沒有該注解以前:
public @interface Authority {
String role();
}
public class RepeatAnnotationUseOldVersion{
@Authority(role="Admin")
@Authority(role="Manager")
public void doSomeThing(){
}
}
類似于這樣的使用編譯器是會報錯的。
通常的做法是:
public @interface Authority {
String role();
}
public @interface Authorities {
Authority[] value();
}
public class RepeatAnnotationUseOldVersion{
@Authorities({@Authority(role="Admin"),@Authority(role="Manager")})
public void doSomeThing(){
}
}
在java 8以後:
@Repeatable(Authorities.class)
public @interface Authority {
String role();
}
public @interface Authorities {
Authority[] value();
}
public class RepeatAnnotationUseNewVersion{
@Authority(role="Admin")
@Authority(role="Manager")
public void doSomeThing(){ }
}
在注解Authority上告訴該注解,如果多次用Authority注解了某個方法,則自動把多次注解Authority作為Authorities注解的成員數組的一個值,當取注解時,可以直接取Authorities,即可取到兩個Authority注解。要求:@Repeatable注解的值的注解類Authorities.class,成員變量一定是被注解的注解Authority的數組。
不同的地方是,建立重複注解Authority時,加上@Repeatable,指向存儲注解Authorities,在使用時候,直接可以重複使用Authority注解。從上面例子看出,java 8裡面做法更适合正常的思維,可讀性強一點。其實從原理上看和第一種是一模一樣的,隻是增加了可讀性。
其實在Java 8中,Annotation還得到了很多很好的擴充。更多Java 8對Annotation提供的改進和支援可參閱:Java 8 Annotation 新特性在軟體品質和開發效率方面的提升
4.Android Support Annotations
Android support library從19.1版本開始引入了一個新的注解庫,它包含很多有用的元注解,開發者可以用他們來修飾代碼,幫助發現bug。Support library自己本身也用到了這些注解,是以作為support library的使用者,Android Studio已經基于這些注解校驗了開發者的代碼并且标注其中潛在的問題。
這些注解是作為一個support包提供給開發者使用,要使用他們,需要在build.gradle中添加對android support-annotations的依賴:
compile 'com.android.support:support-annotations:22.2.0'
提供的注釋概覽如下:

下面分類說明如下:
(1)Nullness Annotations:
作用:check the nullness of a given variable, parameter, or return value.
@Nullable:用于标記方法參數或者傳回值可以為空;
@NonNull:用于标記方法參數或者傳回值不能為空,如果為空編譯器會報警告
(2)Resource Annotations:
主要用于标記方法的參數必須要是指定的資源類型,如果不是,IDE就會給出警告。因為資源檔案都是靜态的,是以在編寫代碼時IDE就知道傳值是否錯誤,可以避免傳的資源id錯誤導緻運作時異常。如下所示:
public abstract void setTitle(@StringRes int resId){ … }
在代碼檢查階段,如果傳遞的引用類型不是R.string類型,編輯器會給出警告。
資源類型注解包括:
@AnimatorRes、@AnimRes、@AnyRes、@ArrayRes、@BoolRes、@ColorRes、@DimenRes、@DrawableRes、@FractionRes、@IdRes、@IntgerRes、@InterpolatorRes、@LayoutRes、@MenuRes、@PluralsRes、@RawRes、@StringRes、@StyleableRes、@StyleRes、@TransitionRes、@XmlRes。
@AnyRes:表示可以是任何資源類型;
(3)Thread Annotations:
作用:用于标記指定的方法、類(如果一個類中的所有方法都有相同的線程需求,就可以對這個類進行注解,比如View.java就被@UIThread所标記)隻能在指定的線程類中被調用。
類型:
@MainThread
@UiThread
@WorkerThread
@BinderThread
@AnyThread
典型使用:A common use of the thread annotation is to validate method overrides in the AsyncTask class class performs background operations and publishes results only on the UI thread.
(4)Value Constraint Annotations(值限制注解):
用于标記參數必須是指定類型的值,并且值的範圍必須在限制的範圍内,包括@Size、@IntRange、@FloatRange。
@IntRange
辨別一個interger或者lang型的參數在一個指定的範圍之内;
public void setAlpha(@IntRange(from=0,to=255) int alpha){ … }
@FloatRange
辨別一個float或者double型的參數在一個指定的範圍之内;
public void setAlpha(@FloatRange(from=0.0, to=1.0) float alpha){...}
@Size
對于資料、集合以及字元串,可以用@Size注解參數來限定集合的大小(當參數是字元串的時候,可以限定字元串的長度)。
集合不能為空: @Size(min=1);
字元串最大隻能有23個字元: @Size(max=23);
數組隻能有2個元素: @Size(2);
(5)Permission Annotations(權限注解):
如果方法需要調用者有特定的權限,可以使用@RequiresPermission注解。
@RequiresPermission(Manifest.permission.SET_WALLPAPER)
public abstract void setWallpaper(Bitmap bitmap) throws IOException;
如果至少需要權限集合中的一個,可以使用anyOf屬性:
@RequiresPermission(anyOf = {
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION})
public abstract Location getLastKnownLocation(String provider);
如果你同時需要多個權限,可以用allOf屬性:
@RequiresPermission(allOf = {
Manifest.permission.READ_HISTORY_BOOKMARKS,
Manifest.permission.WRITE_HISTORY_BOOKMARKS})
public static final void updateVisitedHistory(ContentResolver cr, String url, boolean real)
(6)CallSuper Annotations(複寫方法注解):
如果你的API允許使用者重寫你的方法,但你又需要你自己的方法(父方法)在重寫的時候也被調用,這時候你可以使用@CallSuper标注:
@CallSuper
protected void onCreate(Bundle savedInstanceState){
}
用了這個後,當重寫的方法沒有調用父方法時,工具就會給予警告提示。
5.自定義注解
(1)基本格式:元注解
public @interface 注解名{
定義體;
}
(2)相關說明
注解類型是一種特殊的接口,在聲明時使用的是“@interface”而不是“interface”。
一個注解類型中可以包含多個元素。每個元素都可以看做注解中的配置項,通過方法聲明的形式來定義。這些方法聲明中不能有任何形式參數或類型參數,也不能有抛出受檢異常的throws聲明。方法的名稱是元素的名稱,方法的傳回值類型決定了元素的類型。可以支援的傳回值類型如下:
(1)所有基本資料類型(int,float,boolean,byte,double,char,long,short);
(2)String類型;
(3)Class類型;
(4)enum類型;
(5)Annotation類型;
(6)以上所有類型的數組。
注意點:
(1)注解類中的方法隻能用public或者預設這兩個通路權修飾,不寫public就是預設;
(2)如果注解類中隻有一個成員,最好把方法名設定為”value”;
(3)注解元素必須有确定的值,要麼在定義注解的預設值中指定,要麼在使用注解時指定,非基本類型的注解元素的值不可為null。是以,使用空字元串或0作為預設值是一種常用的做法。;
注解類型中可以沒有任何元素,這種注解類型稱為标記注解類型,是作标記用的。如果注解類型中隻有一個元素,那麼這個元素的名稱應該根據慣例使用value。使用value的好處是可以簡化注解的使用方式,可以為注解類型設定預設值。通過在方法聲明的default關鍵詞來指定。
(3)注解的解析
當注解被添加到java源代碼中後,并不會自動産生作用。某些注解甚至不會在位元組代碼中出現。建立和使用注解隻是完成了第一步,重要的是如何對注解進行相應的處理。在一般情況下,建立和處理注解是Java标準庫和第三方庫應該做的事情,開發人員隻需要使用注解就可以。
根據生存周期不同,可以分為兩種解析方式:運作時解析和編譯時解析。後續會對這兩種解析技術進行詳細的分析說明,并分别基于運作時解析和編譯時解析簡單的模拟實作ButterKnife的效果。
參考文獻