註解(也稱爲元數據
),爲咱們在代碼中添加信息提供了一種形式化的方法,使咱們能夠在稍後某個時刻很是方便的使用這些數據。其中註解是引入到JAVA SE5
的重要的語言變化之一。其能夠提供用來完整的描述程序所需的信息,而這些信息是沒法用Java表達的。所以,註解使得咱們可以以將由編譯器來測試和驗證的格式,存儲有關程序的額外信息。註解能夠用來生成描述符文件。甚至是新的類定義,而且有助於減輕編寫樣板
代碼的負擔。經過使用註解。咱們能夠將這些元數據保存在Java源代碼中,並利用annotation API爲本身的註解構造處理工具
。java
說了這麼多,咱們先來看看註解的聲明。具體參看下面的例子:android
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, ANNOTATION_TYPE,PACKAGE})
public @interface HelloAnnotation {
}
複製代碼
觀看上述例子,咱們發現註解的聲明其實有點相似於Java接口的聲明。除了@符號之外,@HelloAnnotation 定義更像是一個空的接口。事實上,它與其餘任何Java接口同樣。註解也會被編譯成class文件。在定義註解時須要一些元註解
,如@Retention
和@Target
。要知道註解的正確使用,咱們須要瞭解元註解
的使用方法與做用。數組
Java目前內置了5種元註解,元註解主要負責註解其餘的註解。這裏分別對其進行介紹:bash
該註解主要表示該註解能夠用於什麼地方,其中可能ElementType參數爲如下幾種狀況:app
包括註解類型
)或enum聲明JDK1.8加入
)JDK1.8加入
)這裏爲了方便你們理解,會對以上@Target做用範圍進行介紹,其中關於TYPE_PARAMETER與TYPE_USE
會單獨着重介紹。下面咱們仍是以@HelloAnnotation 註解爲例,其中該註解聲明以下:函數
@Target(value = {TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, ANNOTATION_TYPE,PACKAGE})
public @interface HelloAnnotation {
}
複製代碼
那麼在實際代碼中,咱們能夠經過@HelloAnnotation 註解定義的Target去聲明想聲明的東西。具體代碼以下所示:工具
// TYPE:用於類、接口(`包括註解類型`)或enum聲明
@HelloAnnotation
class AnnotationDemo {
//CONSTRUCTOR:用於構造函數聲明
@HelloAnnotation
public AnnotationDemo(String name) {
this.name = name;
}
//FIELD:用於字段聲明,包括enum實例
@HelloAnnotation
private String name;
//METHOD:用於方法聲明
@HelloAnnotation
public void sayHello() {
System.out.println("hello every one");
}
//PARAMETER:用於參數聲明
public void saySometing(@HelloAnnotation String text) { }
//LOCAL_VARIABLE:用於局部變量聲明
public int add(int a, int b) {
@HelloAnnotation int total = 0;
return a + b;
}
//ANNOTATION_TYPE:用於註解可也用於註解聲明(應用於另外一個註解上)
@HelloAnnotation
@interface AnnotationTwo {
}
}
複製代碼
其中對包進行註解修飾,須要在當前包下建立package-info.java
文件,而該文件的做用有如下三點:測試
/**
* 主要是爲了測試包的註解
*/
@HelloAnnotation
package annotation;
複製代碼
在JDK1.8中增長了TYPE_PARAMETER與TYPE_USE
,其中TYPE_PARAMETER用於修飾類上的泛型參數
。ui
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {TYPE_PARAMETER})
public @interface HelloAnnotation {
}
class A<@HelloAnnotation T> {}
複製代碼
TYPE_USE用於任何類型聲明(也包含修飾類上的泛型參數)
,具體狀況以下所示:this
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {TYPE_USE})
public @interface HelloAnnotation {
}
@HelloAnnotation
class AnnotationDemo<@HelloAnnotation T> {
@HelloAnnotation
public AnnotationDemo(String name) {
this.name = name;
}
@HelloAnnotation
private String name;
public void saySometing(@HelloAnnotation String text) { }
public int add(int a, int b) {
@HelloAnnotation int total = 0;
return a + b;
}
@HelloAnnotation
@interface AnnotationTwo { }
}
複製代碼
該註解表該註解在什麼級別下被保存,可選的RetentionPolicy參數爲:
注意:當註解未定義未定義Retention值時,默認值是CLASS級別
該註解表示,是否將註解包含在JavaDoc中,具體列子以下圖所示
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface HelloAnnotation {
}
@HelloAnnotation
class AnnotationDemo {
@HelloAnnotation
public AnnotationDemo(String name) {
this.name = name;
}
@HelloAnnotation
private String name;
public void saySometing(@HelloAnnotation String text) { }
public int add(int a, int b) {
@HelloAnnotation int total = 0;
return a + b;
}
}
複製代碼
咱們跳轉到項目的目錄,打開命令行,執行javadoc -encoding utf-8 -charset utf-8 -package annotation
命令(這裏我是全部的文件都是放在annotaton包下的,因此你能夠根據你本身的包名爲該包下的全部.java文件生成Doc文檔)。運行完命令後咱們找到自動生成的Doc文檔。點擊後以下圖所示:
從上圖中咱們能夠發現,若是爲註解指定了@Documented
元註解,那麼在生成的Doc文檔中是會有相應註解的(如圖上紅箭頭所指
)。
該註解表示,容許子類繼承父類中的註解。其實理解起來也簡單。看下面的列子:
@Target(ElementType.TYPE)
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@interface Hello {
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface World {
}
@Hello
class Person {
}
@World
class Man extends Person {
public static void main(String[] args) {
Annotation[] annotations = Man.class.getAnnotations();
for (Annotation annotation : annotations) {
System.out.println(annotation.annotationType());
}
}
}
//輸出結果
interface annotation.Hello
interface annotation.World
複製代碼
在上述代碼中,建立了@Hello與@World註解,其中@Hello使用@Inherited修飾
,同時咱們也建立了用@Hello修飾的Person類
及其用@World修飾的Man子類
,而後咱們經過Man.class.getAnnotations()
方法獲取Man類中的註解(下文會對註解使用以及賦值進行介紹),獲得上述代碼中的輸出結果。也就證實了@Inherited元註解可讓子類繼承父類中的註解的結論。
該註解是JDK 1.8新加入的,該註解表示,能夠在同一個地方屢次使用同一種註解類型
。也就是說在JDK 1.8以前是沒法在同一個類型上使用相同的註解的。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface FilterPath {
String value();
}
//在JDK1.8以前是在一個地方屢次使用同一種註解
@FilterPath("hello/java")
@FilterPath("hello/android")
class FileOperate {
}
複製代碼
在上述代碼中,咱們聲明瞭@FilterPath註解,你有可能注意到了其中的String value();
這段語句,這裏你們先不着急理解這段代碼究竟是什麼意思,你們就理解成給該註解提供字符串賦值
操做就好了(下文會對註解使用以及賦值進行介紹)。若是咱們採用以上代碼,編譯器是會報錯的。因此爲了處理這種狀況,在JDK1.8以前,若是想實現相似於上述相同的功能,咱們通常採用下面的這種方式:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface FilterPath {
String []value();
}
@FilterPath({"hello/java","hello/android"})
class FileOperate {
}
複製代碼
將@FilterPath註解中的String value();
修改成String []value();
,也就是說讓該註解接受字符串數組。
在JDK1.8以後咱們可使用@Repeatable,可是使用該註解也有必定的限制。下面咱們一塊兒來看看:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(FilterPaths.class)//添加@Repeatable元註解,注意其中的值
public @interface FilterPath {
String value();
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface FilterPaths {
FilterPath[] value();//註解其中的數組類型爲FilterPath
}
//如今能夠在同一個地方使用同一註解啦~
@FilterPath("hello/java")
@FilterPath("hello/android")
class FileOperate {
}
複製代碼
在上述代碼中,咱們建立了@FilterPath與@FilterPaths
兩個註解,須要注意的是咱們在@FilterPath註解上增長了元註解@Repeatable(FilterPaths.class)
其中的參數FilterPaths.class是指明接受同一個類型上重複註解的容器,
(也就是接受重複的@FilterPath註解),那麼咱們再看@FilterPaths中聲明瞭 FilterPath[] value();
,也就是其接受@FilterPath註解類型。
爲了處理@Repeatble註解,JDK1.8在AnnotatedElement接口中提供了getAnnotationsByType與getAnnotationsByType
方法,(注意:若是咱們採用傳統的方法,也就是getAnnotation(Class<A> annotationClass)
方法來獲取聲明的註解,咱們須要傳入註解容器的class
,而不是須要重複的註解的class
)。這裏咱們仍是以JDK1.8以後中提到的代碼爲例:
public static void main(String[] args) {
//從該類上獲取FilterPath註解信息
FilterPath filterPath = FileOperate.class.getAnnotation(FilterPath.class);
System.out.println(filterPath);
//從該類上獲取FilterPaths註解信息
System.out.println("----------------------------");
FilterPaths filterPaths = FileOperate.class.getAnnotation(FilterPaths.class);
System.out.println(filterPaths);
for (FilterPath path : filterPaths.value()) {
System.out.println(path.value());
}
//經過getAnnotationsByType
System.out.println("----------------------------");
FilterPath[] annotationsByType = FileOperate.class.getAnnotationsByType(FilterPath.class);
if (annotationsByType != null) {
for (FilterPath path : annotationsByType) {
System.out.println(path.value());
}
}
//經過getDeclaredAnnotationsByType
System.out.println("----------------------------");
FilterPath[] declaredAnnotationsByType = FileOperate.class.getDeclaredAnnotationsByType(FilterPath.class);
if (declaredAnnotationsByType != null) {
for (FilterPath path : declaredAnnotationsByType) {
System.out.println(path.value());
}
}
}
//輸出結果
null
----------------------------
@annotation.FilterPaths(value=[@annotation.FilterPath(value=hello/java), @annotation.FilterPath(value=hello/android)])
hello/java
hello/android
----------------------------
hello/java
hello/android
----------------------------
hello/java
hello/android
複製代碼
從輸出結果來看,咱們並不能經過getAnnotation(FilterPath.class)
獲取註解(得到的註解爲null),而是須要經過getAnnotation(FilterPaths.class
)來獲取)。同時若是咱們採getAnnotationsByType(FilterPath.class)
或getDeclaredAnnotationsByType(FilterPath.class)
就能獲取到正確的值。這裏須要注意getAnnotationsByType
與getDeclaredAnnotationsByType
方法的區別,若是子類調用getAnnotationsByType
方法且該子類的父類中聲明瞭用@Inherited修飾的註解,那麼能夠得到父類中的註解。而getDeclaredAnnotationsByType
是獲取不到父類中聲明的註解的。
在瞭解了註解的定義與元註解以後,咱們一塊兒來了解註解元素中能夠定義的屬性。其中支持的具體類型以下所示:
若是你使用了其餘類型,那麼編譯器就會報錯,注意!!!!也不能使用任何類型的包裝類型
。不過因爲自動打包的存在,這也不算什麼限制。註解亦能夠做爲元素的類型。也就是說註解能夠嵌套
。
咱們已經知道了註解中屬性的支持類型,如今就開始爲註解添加屬性吧。其基本語法是: 類型 屬性名();
,請看以下例子:
//聲明枚舉
enum Week {
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY,
SUNDAY
}
@Target(ElementType.TYPE_USE)
@Retention(RetentionPolicy.RUNTIME)
public @interface HelloAnnotation {
String text();
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface WorldAnnotation {
//基本類型及其數組類型
int intAttr();
float floatAttr() ;
boolean booleanAttr() ;
int[] intArray() ;
float[] floatArray() ;
boolean[] booleanArry() ;
//String類型及其數組類型
String stringAttr() ;
String[] stringArray();
//Class類型及其數組類型
Class classAttr() ;
Class[] classArray();
//enum類型及其數組類型
Week day() ;
Week[] week();
//註解嵌套
HelloAnnotation HelloAnnotation();
}
複製代碼
在上圖中我將註解的支持的全部類型都展現出來了,這樣我相信你們都能很是好的理解了。
屬性除了用戶指定值外,還支持默認值,其語法爲:屬性 屬性名() default 默認值;
,那麼結合上述的例子:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface WorldAnnotation {
//基本類型及其數組類型
int intAttr() default -1;
float floatAttr() default -1f;
boolean booleanAttr() default false;
int[] intArray() default {1, 2, 3};
float[] floatArray() default {1f, 2f, 3f};
boolean[] booleanArry() default {true, false, true};
//String類型及其數組類型
String stringAttr() default "";
String[] stringArray();
//Class類型及其數組類型
Class classAttr() default Class.class;
Class[] classArray();
//enum類型
Week day() default Week.MONDAY;
//enum數組類型
Week[] week() default {Week.MONDAY,Week.THURSDAY};
//註解嵌套
HelloAnnotation HelloAnnotation() default @HelloAnnotation(text = "word");
}
複製代碼
也就是說當用戶本身沒有指定相應屬性值的時候,若是屬性設置了默認值,那麼該屬性的值就是默認值。可是設置屬性的默認值時有限制的。具體內容看下面的介紹。
雖然限制咱們已經能夠在註解定義咱們想要的信息,可是在Java中,註解中的元素類型必需要麼有默認值,要麼在使用註解是提供元素的值。其次對於非基本類型的元素
,不管是在源代碼中聲明時,或是在註解接口中定義默認值時,都不能以null做爲其值
。這個約束使得處理器很難發現一個元素的存在和缺失的狀態,由於在每一個註解的聲明中,全部的元素都存在,而且都具備相應的值,爲了繞開這個約束,咱們只能定義一些特殊的值,例如空字符串或負數,以此表示某個元素不存在
。也就說像這樣的代碼編譯器是不會經過的:
String stringAttr() default null;//錯誤!!!!
String[] stringArray() default null;//錯誤!!!
複製代碼
若是一個註解中有一個名稱爲value
的屬性,且你只想設置value屬性(即其餘屬性都採用默認值或者你只有一個value屬性),那麼能夠省略掉「value=」
部分。具體代碼以下:
//第一種狀況,只有一個vaule屬性,那麼你在使用時候能夠直接@WorldAnnotation("hello")
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface WorldAnnotation {
String value();
}
//第二種狀況,有多個屬性可是其屬性都有默認值,
//你只使用value屬性,那麼你在使用時候能夠直接@WorldAnnotation("hello")
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface WorldAnnotation {
int intAttr() default -1;
float floatAttr() default -1f;
String value();
}
複製代碼
在瞭解了註解的定義與屬性的添加後,如今咱們在來看看註解的實際運用狀況。註解的使用須要與Java的反射機制
結合使用。因此瞭解其中的瞭解二者以前的關係尤其重要。
衆所周知,JAVA反射機制是在運行狀態中,對於任意一個類,都可以知道這個類的全部屬性和方法;對於任意一個對象,都可以調用它的任意方法和屬性;這種動態獲取信息以及動態調用對象方法的功能稱爲java語言的反射機制。那麼從Java的整個類加載機制來看,過程是以下這樣:
對於Java類的加載主要分爲如下步驟:
*.java
文件經過javac命令編譯成擴展名爲*.class
文件。其中*.class
文件保存着Java代碼轉換後的虛擬機指令。*.class
文件,並建立對應的Class對象
。其中Class對象中不只有着類的聲明定義,還有 Constructor(類的構造器定義)、Field(類的成員變量定義)、Method(類的方法定義)、Package(類的包定義)。當咱們聲明瞭註解,且將註解的生命週期設置爲@Retention(RetentionPolicy.RUNTIME)
,那麼在編譯成class文件的時候,會將註解添加到文件中去
,那麼JVM根據class文件生成相應的Class對象以後就會帶有註解信息
。那麼咱們經過Class對象中的Constructor、Field、Method等類,就能獲取其上聲明的註解信息了。那註解信息究竟是以聲明形式聲明與表現的呢?
這裏咱們仍是以@HelloAnnotation 註解爲例,當咱們聲明瞭註解後,經過javap
命令獲取編譯後的的字節碼信息
@Target(ElementType.TYPE_USE)
@Retention(RetentionPolicy.RUNTIME)
public @interface HelloAnnotation {
String value();
}
//經過javap -p HelloAnnotation.class
public interface annotation.HelloAnnotation extends java.lang.annotation.Annotation {
public abstract java.lang.String value();
}
複製代碼
從上圖中,咱們能夠得知通過編譯後,其實註解最終會繼承Annotation接口
。也就是說註解最終會以java.lang.annotation.Annotation
對象的形式在Class對象中進行展現或存儲。
爲了方便處理接口信息以及實現面向對象的規則,其中Constructor、Field、Method、Class、Package
類都實現了AnnotatedElement
接口。具體關係以下圖所示:
也就是最終的註解註解處理所有都交給了AnnotatedElement
接口來實現。那如今咱們來看看該接口的方法聲明。
在AnnotatedElement
接口中爲咱們提供瞭如下幾個方法來獲取註解信息:
方法名稱 | 返回值 | 方法說明 |
---|---|---|
getAnnotation(Class annotationClass) | <T extends Annotation> T |
返回元素上指定類型的註解,若是無,則返回爲null |
getAnnotations() | Annotation[ ] | 返回元素上存在的全部註解,包括從父類繼承的 |
getAnnotationsByType(Class annotationClass) since 1.8 |
<T extends Annotation> T [ ] |
返回元素上指定的類型的註解數組,包括父類的註解 ,若是無,返回長度爲0的數組,該方法與getAnnotation(Class annotationClass)的主要區別是,該方法能夠檢查註解是否是重複的 。若是是這樣,嘗試經過「查看」容器註釋來找到該類型的一個或多個註釋。 |
getDeclaredAnnotation(Class annotationClass) | <T extends Annotation> T |
返回該元素上的指定類型的全部的註解,不包括父類的註解,若是無,返回長度爲0的數組 |
getDeclaredAnnotationsByType(Class annotationClass) since 1.8 |
<T extends Annotation> T [ ] |
同getAnnotationsByType(Class annotationClass)方法相似,只是獲取的註解中不包括父類的註解 |
getDeclaredAnnotations() | Annotation[ ] | 返回該元素上的全部的註解,不包括父類的註解,若是無,返回長度爲0的數組 |
其中getAnnotationsByType(Class<T> annotationClass)
與getDeclaredAnnotationsByType(Class<T> annotationClass)
方法是jdk 1.8
以後提供的接口默認實現方法
。須要注意的是該兩個方法是支持註解的@Repeatable
,而其餘方法是不支持的。那麼在平時開發中,咱們能夠根據本身的項目需求選取不一樣的方法。
經過了解註解的聲明以及與反射機制之間的關係後,如今咱們來實戰一下。簡單的寫個例子完全鞏固註解的相關知識吧。這裏咱們簡單經過什麼人在什麼地方作了什麼事
爲例子:
//什麼人
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Who {
String name();
int age();
}
//在哪裏
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface Where {
String country();
String province();
String city();
}
//作了什麼事
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface DoSomething {
String value();
}
複製代碼
上述代碼中,咱們聲明瞭三個註解,若是你認真看了前面咱們說的註解的定義和使用話的理解起來很是簡單,這裏咱們須要注意的是三個註解中的 @Retention
都是設置爲(RetentionPolicy.RUNTIME)
,之因此設置爲運行時,是由於根據類的加載機制,Class對象的生成是在JVM讀取class文件的時候,也就是運行期間。那下面咱們接着看具體的使用:
@Who(name = "AndyJenifer", age = 18)
class Person {
@Where(country = "中國", province = "四川", city = "成都")
private String where;
@DoSomething("寫博客")
public void doSomething() {
}
public static void main(String[] args) {
Class<Person> personClass = Person.class;
StringBuffer sb = new StringBuffer();
//獲取類上的註解
Who who = personClass.getAnnotation(Who.class);
sb.append(who.name());
sb.append(who.age());
//獲取字段上的註解
Field[] fields = personClass.getDeclaredFields();
for (Field field : fields) {
Annotation[] annotations = field.getAnnotations();
if (annotations.length > 0 && annotations[0] instanceof Where) {
Where where = (Where) annotations[0];
sb.append(where.country());
sb.append(where.province());
sb.append(where.city());
}
}
//獲取方法上的註解
Method[] methods = personClass.getMethods();
for (Method method : methods) {
Annotation[] annotations = method.getAnnotations();
if (annotations.length > 0 && annotations[0] instanceof DoSomething) {
DoSomething doSomething = (DoSomething) annotations[0];
sb.append(doSomething.value());
}
}
System.out.println(sb.toString());
}
//輸出結果:AndyJenifer18中國四川成都寫博客
}
複製代碼
上述代碼理解起來仍是比較容易,在Main方法中獲取當前Person的Class對象,經過Class對象獲取其中聲明的字段與方法。獲得相應的字段與方法後,再去拿上面聲明的註解。而後組合信息並打印。細心的小夥伴確定觀察到了在獲取相應字段的時候,咱們調用的是getDeclaredFields()
而不是方法getFields()
(固然對於其餘元素,如 Constructor、Field、Method、Package,都有相似的方法getDeclaredXXXX()
與getXXXX()
)。這裏簡單的說一下這兩種方法的區別:
public、private和proteced
,可是不包括父類聲明的
。文章到這裏,如今你們已經基本瞭解了註解的聲明與使用。不知道小夥伴們有沒有想過一個問題。若是咱們聲明瞭一個註解,而後但願該註解在項目的不一樣類中都會使用。那麼當處理這些類的註解的時候,咱們是否是須要手動的找到全部的Class對象
?(無論你是經過類名也好,仍是經過文件的方式來讀取也好)。那這樣是否是會很麻煩呢?在文章中咱們也提到過,註解能夠用來生成描述符文件。甚至是新的類定義,而且有助於減輕編寫樣板
代碼的負擔。那麼怎麼經過註解生成新的類的定義呢?
,又怎麼生成樣板代碼呢?
。若是你們有興趣,咱們將在後續文章繼續講述並解決這些問題。
該文章參考如下博客與圖書,站在巨人的肩膀上。能夠看得更遠。
深刻理解Java註解類型(@Annotation) 《Think in java 》