java註解的基礎講解
使用Java註解來簡化你的程式碼
2017年04月18日 14:09:02
閱讀數:847
註解(Annotation)就是一種標籤,可以插入到原始碼中,我們的編譯器可以對他們進行邏輯判斷,或者我們可以自己寫一個工具方法來讀取我們原始碼中的註解資訊,從而實現某種操作。需要申明一點,註解不會改變編譯器的編譯方式,也不會改變虛擬機器指令執行的順序,它更可以理解為是一種特殊的註釋,本身不會起到任何作用,需要工具方法或者編譯器本身讀取註解的內容繼而控制進行某種操作。本篇文章將從以下幾點詳細的介紹下Java註解的使用:
- 元資料和註解(Annotation)
- 按照引數個數分類註解(標記,單值,完整)
- 按照註解使用途徑分類(標準,元註解,自定義)
- 自定義註解處理器完成讀取註解內容的操作
一、元資料和註解
元資料(meta-data)就是指用來描述資料的資料,它往往是以標籤的形式出現,主要用於描述程式碼塊之間的聯絡。我們的註解就是一種元資料,根據它所起到的作用,我們可以大致將它分為以下三類:
- 編寫文件:通過程式碼中標識的元資料生成文件
- 程式碼分析:通過程式碼中的元資料獲取其中資訊內容
- 編譯檢查:通過標記註解可以完成對程式碼塊的檢查,例如:@Override,用於檢查格式
二、標準註解(系統自帶)
在我們jdk的java.lang包中定義了三個註解,他們是:@Override,@Deprecated,@SuppressWarnnings。Override這個註解我們經常會使用到,在子類重寫父類方法的時候就會使用到,他會幫助我們校驗格式,確保我們正在定義的方法是在重寫了父類的對應方法。Deprecated註解一般修飾在類或者方法之前,用於表示該方法或者類已經不再推薦使用了。SuppressWarnnings註解主要用於抑制編譯器警告,具體的我們簡單的演示下。
public class People { public void sayHello(){ System.out.println("helo walker"); } } public class Student extends People { /*@Override public void sayHello(){ System.out.println("hello yam"); }這樣是沒有問題的*/ @Override public void say(){ System.out.println("hello yam"); }/*如果你定義的方法不能重寫父類某個方法,要麼拼寫錯誤,引數個數,方法名不一樣等,編譯丟擲警告*/ }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
我們需要注意的是,這裡的override註解只能用於修飾方法,不能用於修飾類或者域。
public class Student extends People {
@Deprecated
public void say(){
System.out.println("hello yam");
}
}
//呼叫過時方法
public static void main(String[] args){
Student s = new Student();
s.say();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
雖然編譯時丟擲了警告,但是程式依然可以正常的執行結束。此註解只是告知使用者被標記的方法或者類已經不再推薦使用,但是你依然是可以使用的。之所以建議不再使用,一定是有了更好的取代物了,如果你一定要在你的專案中使用,等待新的jdk版本釋出之後,很可能刪除了這些方法或者類,可能會導致你的專案原先的一些方法或者類無法識別。
@SuppressWarnings("deprecation")
public static void main(String[] args){
Student s = new Student();
s.say();
}
- 1
- 2
- 3
- 4
- 5
例如,我們可以使用SuppressWarnings註解,阻止彈出過時警告。關於SuppressWarnings的引數主要有以下幾種:
- deprecation:使用了不贊成使用的類或方法時的警告
- unchecked:執行了未檢查的轉換時的警告,例如當使用集合時沒有用泛型 (Generics) 來指定集合儲存的型別;
- fallthrough:當 Switch 程式塊直接通往下一種情況而沒有 Break 時的警告;
- path:在類路徑、原始檔路徑等中有不存在的路徑時的警告;
- serial:當在可序列化的類上缺少 serialVersionUID 定義時的警告;
- finally:任何 finally 子句不能正常完成時的警告;
- all:關於以上所有情況的警告。
三、元註解
元註解就是用來註解註解的註解。定義可能有點繞,其實元註解是一種註解,他可以加在一般的註解上用於限制該註解的使用範圍,生命週期等。一般在自定義註解時候使用的多。在jdk的中java.lang.annotation包中定義了四個元註解:
- @Target:指定被修飾的註解的作用範圍
- @Retention:指定了被修飾的註解的生命週期
- @Documented:指定了被修飾的註解是可以被例如Javadoc等工具文件化的
- @Inherited:指定了被修飾的註解修飾程式元素的時候是可以被子類繼承的
我們首先看看@Target的使用:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
- 1
- 2
- 3
- 4
- 5
這是系統註解Override的定義原始碼,我們看到Target註解中引數ElementType.METHOD表示該註解只能用於修飾方法。使用Target註解限定了Override的修飾範圍只能使方法,不能是類或者域。Target還有一些其他的引數:
- CONSTRUCTOR:用於描述構造器
- FIELD:用於描述域
- LOCAL_VARIABLE:用於描述區域性變數
- METHOD:用於描述方法
- PACKAGE:用於描述包
- PARAMETER:用於描述引數
- TYPE:用於描述類、介面(包括註解型別) 或enum宣告
通過上述的引數我們可以在定義一個註解的時候限定他的作用範圍。
下面看看Retention這個元註解,依然以註解Override為例,
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
- 1
- 2
- 3
- 4
我們看到Retention中使用了引數RetentionPolicy.SOURCE,這個引數表示該註解只在原始碼中有效,進過編譯之後將會被丟棄。還有一些其他引數:
- SOURCE:在原始檔中有效(即原始檔保留)
- CLASS:在class檔案中有效(即class保留)
- RUNTIME:在執行時有效(即執行時保留)
SOURCE表示編譯器編譯之後的class檔案中是不存在這一行註解程式碼的,CLASS範圍表示編譯器編譯之後,註解程式碼存在於class檔案中,但是jvm在載入此class檔案的時候會自動忽略掉這一行註解程式碼。RUNTIME表示jvm載入class檔案的時候會被讀取到記憶體,也就是執行時保留。
接著使註解Documented,這是一個關於文件的元註解,被它註解的註解在註解其他方法或者類的時候可以被Javadoc等工具文件化,對於一般的註解,在Javadoc等工具文件化類或者方法的時候會丟棄註解內容,使用它就可以使得文件化的時候依然儲存著註解程式碼。
//Test是一個被元註解Documented修飾
public class User {
@Test(value = 10,description = "do something")
public void test1() {
}
}
- 1
- 2
- 3
- 4
- 5
- 6
使用Javadoc生成API:
類User中的方法test1方法的頭部是保留著註解的,如果是一般的註解則不會保留。
最後是元註解Inherited,我們知道如果一個普通的註解修飾了一個父類,那麼他的子類是不能繼承修飾父類的註解的。
@Deprecated
public class People {
public void sayHello(){
System.out.println("helo walker");
}
}
public class Student extends People {
public void say(){
System.out.println("hello yam");
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
我們可以看到,在父類people上使用了註解Deprecated,people類名上是有刪除線的(貼上到此處並沒有顯示)表示此類不推薦使用,但是我們可以看到在子類Student上是沒有刪除線的,也就是父類廢棄了,子類依然是正常的。(註解不會被繼承),但是如果我們希望子類能夠繼承父類的某些註解,那麼只需要在定義該註解的時候使用我們的元註解Inherited修飾即可。
四、自定義註解
以上我們看到的標準註解,元註解都是jdk中定義好了的,如果我們想要自定義一個自己的註解就需要通過@interface來定義一個全新的註解。
//定義一個註解
public @interface myAnnotion {
}
- 1
- 2
- 3
- 4
使用@interface定義一個註解的時候,會自動繼承java.lang.annotation.Annotation介面,以下是其中的一些方法:
boolean equals(Object obj);
int hashCode();
String toString();
Class<? extends Annotation> annotationType();
- 1
- 2
- 3
- 4
我們自定義的註解,除了多了個@符號,其他的和定義一個介面是一樣的,所以這些方法我們不用實現。以上我們定義的是一個沒有註解體的一個註解,像這樣的註解我們叫做標記註解,這是表示一種標記,編譯器根據某個類或方法是否具有此標記來判斷是否要新增一些程式碼或做一定的檢測操作。例如:@Override註解就是一個標記註解,如果某個方法前被修飾了此註解,編譯器在編譯時會找到父類,判斷對應的方法是否完成了重寫的格式。
下面聲明瞭一個具有註解體的註解:
public @interface myAnnotion {
String name() default "";
int age();
}
- 1
- 2
- 3
- 4
- 5
我們說過,宣告註解和宣告介面很是類似,所以註解中的所有引數都必須以抽象方法的形式存在,例如上面一樣。接下來我們看如何使用該註解:
@myAnnotion(name = "walker",age=10)
public class Test_ann {
public static void main(String[] args){
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
之前我們說過,註解本身不會起到任何作用,需要配合註解處理器才能發揮一定的作用,自己本身其實更像是一種特殊的註釋。在上例中,我們可以在()中為註解的內部引數賦值,需要注意的是,註解的引數不允許為null,也就是在使用註解的時候,內部的每個引數都是必須要有數值的,要麼在定義的時候給賦上預設值(使用default關鍵字),要麼在()內顯式的賦值。允許的註解引數型別有:
- 所有基本資料型別(int,float,boolean,byte,double,char,long,short)
- String型別
- Class型別
- enum型別
- Annotation型別
- 以上所有型別的陣列
如果我們想要表示註解中某個引數不存在,該怎麼辦呢?比如我們用上述自定義的註解去修飾了一個People類,如果此人的age不知道,我們該如何賦值(引數的值不能為null)。我們往往用一些特殊值來標記某個引數不存在的情況,例如我們可以給age賦值-1表示此人年齡不詳,在使用註解處理器讀取的時候發現age等於-1,我們就知道此人年齡不詳。往往字串型別的引數用”“表示引數不存在,整型型別引數使用負數表示引數不存在。
五、使用註解處理器響應註解
我們說過一個註解被定義出來之後,是不能完成任何作用的,如果沒有註解處理器響應的註解和註釋差不多。本小節我們看看如何定義一個註解處理器來對我們自定義的註解進行響應。還有一個前提是:我們的註解處理器實際上也是類,所以它只有在被載入到jvm中才能生效,但是如果我們的註解的生命週期範圍到不了jvm的話,註解處理器也是沒用的。
Java擴充了其反射機制,使得我們可以利用反射來獲取註解資訊。反射中的Class,Method,Constructor,Field,Package都繼承了介面AnnotatedElement,這個介面主要有以下幾個方法:
/*判斷是否存在指定的註解*/
boolean isAnnotationPresent(Class<? extends Annotation> annotationClass)
/*獲取指定的註解*/
<T extends Annotation> T getAnnotation(Class<T> annotationClass);
/*獲取當前元素的所有註解*/
Annotation[] getAnnotations();
/*返回直接存在於此元素上的指定的註解,忽略繼承,如果沒有返回null*/
<T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass)
/*返回直接存在於此元素上的所有註解,忽略繼承*/
Annotation[] getDeclaredAnnotations();
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
下面看一個註解的簡單總和例項:
@Target(value={ElementType.FIELD})//修飾Filed的註解
@Retention(value = RetentionPolicy.RUNTIME ) //執行時保留
public @interface PName {
String name() default "";
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
@Target(value={ElementType.FIELD})//修飾Filed的註解
@Retention(value = RetentionPolicy.RUNTIME ) //執行時保留
public @interface PAge {
int age() default 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
@Target(value={ElementType.METHOD})//修飾method的註解
@Retention(value = RetentionPolicy.RUNTIME ) //執行時保留
public @interface SayHello {
String content() default "hello";
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
public class People {
@PName(name = "people")
private String name;
@PAge(age = 20)
private int age;
@SayHello(content = "hello people")
public void sayHello(){
System.out.println("hello people");
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
public static void main(String[] args) throws NoSuchMethodException {
//獲取people類中所有註解資訊
Field[] fields = People.class.getDeclaredFields();
for(Field f : fields){
//遍歷每個屬性
if(f.isAnnotationPresent(PName.class)){
PName pn = f.getAnnotation(PName.class);
System.out.println(pn.name());
}else{
PAge pa = f.getAnnotation(PAge.class);
System.out.println(pa.age());
}
}
Method md = People.class.getMethod("sayHello");
SayHello sh = md.getAnnotation(SayHello.class);
System.out.println(sh.content());
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
上述的程式碼完成了將people類中所有註解資訊全部獲取列印的工作。這個例子可能不能準確的描述註解在我們程式中的作用(起碼註解不會用來幹這個),但是在一方面演示了定義到使用註解的過程,希望對大家在專案中實際使用有所啟發。
最後,本篇文章結束了,望大家多多留言交流,相互學習。