Java 元註解入門介紹

註解(Annotation)相當於一種標記,在程序中加入註解就等於爲程序打上某種標記,沒有加,則等於沒有任何標記,以後,javac編譯器、開發工具和其他程序可以通過反射來了解你的類及各種元素上有無何種標記,看你的程序有什麼標記,就去幹相應的事,標記可以加在包、類,屬性、方法,方法的參數以及局部變量上。

元註解

元註解是什麼意思呢?

元註解是可以註解到註解上的註解,或者說元註解是一種基本註解,但是它能夠應用到其它的註解上面。

如果難於理解的話,你可以這樣理解。元註解也是一張標籤,但是它是一張特殊的標籤,它的作用和目的就是給其他普通的標籤進行解釋說明的。

元標籤有 @Retention、@Documented、@Target、@Inherited、@Repeatable 5 種。

@Retention元註解

@Retention定義了該Annotation被保留的時間長短:某些Annotation僅出現在源代碼中,而被編譯器丟棄;而另一些卻被編譯在class文件中;編譯在class文件中的Annotation可能會被虛擬機忽略,而另一些在class被裝載時將被讀取。

當 @Retention 應用到一個註解上的時候,表示需要在什麼級別保存該註釋信息,用於描述註解的生命週期(即:被描述的註解在什麼範圍內有效)

它的取值如下: 
- RetentionPolicy.SOURCE 註解在源文件中有效,即只在源碼階段保留,在編譯器進行編譯時它將被丟棄忽視。 
- RetentionPolicy.CLASS 註解只被保留到編譯進行的時候,它並不會被加載到 JVM 中。 
- RetentionPolicy.RUNTIME 註解在運行時有效,它會被加載進入到 JVM 中,所以在程序運行時可以獲取到它們。

範圍大小順序爲:Java源文件(.java文件)---->.class文件---->內存中的字節碼

@Retention(RetentionPolicy.RUNTIME)
public @interface AnnotationDemo {
}

 

@Target元註解

@Target說明了Annotation所修飾的對象範圍:Annotation可被用於 packages、types(類、接口、枚舉、Annotation類型)、類型成員(方法、構造方法、成員變量、枚舉值)、方法參數和本地變量(如循環變量、catch參數)。在Annotation類型的聲明中使用了target可更加明晰其修飾的目標。

就像一張標籤,原本標籤是你想張貼到哪個地方就到哪個地方,但是因爲 @Target 的存在,它張貼的地方就非常具體了,比如只能張貼到方法上、類上、方法參數上等等。

@Target 有以下取值

  • ElementType.ANNOTATION_TYPE 可以給一個註解進行註解
  • ElementType.CONSTRUCTOR 可以給構造方法進行註解
  • ElementType.FIELD 可以給屬性進行註解
  • ElementType.LOCAL_VARIABLE 可以給局部變量進行註解
  • ElementType.METHOD 可以給方法進行註解
  • ElementType.PACKAGE 可以給一個包進行註解
  • ElementType.PARAMETER 可以給一個方法內的參數進行註解
  • ElementType.TYPE 可以給一個類型進行註解,比如類、接口、枚舉 

註解Table 可以用於註解類、接口(包括註解類型) 或enum聲明,而註解NoDBColumn僅可用於註解類的成員變量

@Target(ElementType.TYPE)
public @interface Table {
    /**
     * 數據表名稱註解,默認值爲類名稱
     * @return
     */
    public String tableName() default "className";
}

@Target(ElementType.FIELD)
public @interface NoDBColumn {

}

@Inherited元註解

 @Inherited 元註解是一個標記註解,@Inherited闡述了某個被標註的類型是被繼承的。如果一個超類被 @Inherited 註解過的註解進行註解的話,那麼如果它的子類沒有被任何註解應用的話,那麼這個子類就繼承了超類的註解。

注意:@Inherited annotation類型是被標註過的class的子類所繼承。類並不從它所實現的接口繼承annotation,方法並不從它所重載的方法繼承annotation。

@Inherited
@Retention(RetentionPolicy.RUNTIME)
@interface Test {}


@Test
public class A {}


public class B extends A {}

 註解 Test 被 @Inherited 修飾,之後類 A 被 Test 註解,類 B 繼承 A,類 B 也擁有 Test 這個註解

@Documented元註解

顧名思義,這個元註解肯定是和文檔有關。它的作用是能夠將註解中的元素包含到 Javadoc 中去。

@Repeatable元註解

@Repeatable可重複

舉個例子,一個人他既是程序員又是產品經理,同時他還是個畫家

//value 的屬性,屬性類型是一個被 @Repeatable 註解過的註解數組,注意它是數組
@interface Persons {
    Person[]  value();
}

//@Repeatable 註解了 Person。而 @Repeatable 後面括號中的類相當於一個容器註解
@Repeatable(Persons.class)
@interface Person{
    String role default "";
}


@Person(role="artist")
@Person(role="coder")
@Person(role="PM")
public class SuperMan{

}

註解的屬性

註解的屬性也叫做成員變量。註解只有成員變量,沒有方法。註解的成員變量在註解的定義中以“無形參的方法”形式來聲明,其方法名定義了該成員變量的名字,其返回值定義了該成員變量的類型。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {

    int id();

    String msg();

}

上面代碼定義了 TestAnnotation 這個註解中擁有 id 和 msg 兩個屬性。在使用的時候,我們應該給它們進行賦值。

賦值的方式是在註解的括號內以 value=”” 形式,多個屬性之前用 ,隔開。

@TestAnnotation(id=3,msg="hello annotation")
public class Test {

}

在註解中定義屬性時它的類型必須是 8 種基本數據類型外加 類、接口、註解及它們的數組。

註解中屬性可以有默認值,默認值需要用 default 關鍵值指定。比如:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {

    public int id() default -1;

    public String msg() default "Hi";

}

TestAnnotation 中 id 屬性默認值爲 -1,msg 屬性默認值爲 Hi。 
它可以這樣應用。

@TestAnnotation()
public class Test {}

因爲有默認值,所以無需要再在 @TestAnnotation 後面的括號裏面進行賦值了,這一步可以省略。

另外,還有一種情況。如果一個註解內僅僅只有一個名字爲 value 的屬性時,應用這個註解時可以直接接屬性值填寫到括號內

public @interface Check {
    String value();
}

上面代碼中,Check 這個註解只有 value 這個屬性。所以可以這樣應用。

@Check("hi")
int a;
//或者
@Check(value="hi")
int a;

最後,還需要注意的一種情況是一個註解沒有任何屬性。比如

public @interface Perform {}

那麼在應用這個註解的時候,括號都可以省略

@Perform
public void testMethod(){}

註解的提取

通過用標籤來比作註解,前面的內容是講怎麼寫註解,然後貼到哪個地方去,而現在我們要做的工作就是檢閱這些標籤內容。 形象的比喻就是你把這些註解標籤在合適的時候撕下來,然後檢閱上面的內容信息。

要想正確檢閱註解,離不開一個手段,那就是反射。

註解與反射

註解通過反射獲取。首先可以通過 Class 對象的 isAnnotationPresent() 方法判斷它是否應用了某個註解

public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {}

然後通過 getAnnotation() 方法來獲取 Annotation 對象。

 public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {}

或者是 getAnnotations() 方法。


public Annotation[] getAnnotations() {}

前一種方法返回指定類型的註解,後一種方法返回註解到這個元素上的所有註解。

如果獲取到的 Annotation 如果不爲 null,則就可以調用它們的屬性方法了。比如

@TestAnnotation()
public class Test {

    public static void main(String[] args) {

        boolean hasAnnotation = Test.class.isAnnotationPresent(TestAnnotation.class);

        if ( hasAnnotation ) {
            TestAnnotation testAnnotation = Test.class.getAnnotation(TestAnnotation.class);

            System.out.println("id:"+testAnnotation.id());
            System.out.println("msg:"+testAnnotation.msg());
        }

    }

}

 

程序的運行結果是:

id:-1
msg:

這個正是 TestAnnotation 中 id 和 msg 的默認值。

如果一個註解要在運行時被成功提取,那麼 @Retention(RetentionPolicy.RUNTIME) 是必須的。 

實例演示

寫一個測試框架,測試程序員的代碼有無明顯的異常。

—— 程序員 A : 我寫了一個類,它的名字叫做 NoBug,因爲它所有的方法都沒有錯誤。 
—— 我:自信是好事,不過爲了防止意外,讓我測試一下如何? 
—— 程序員 A: 怎麼測試? 
—— 我:把你寫的代碼的方法都加上 @Jiecha 這個註解就好了。 
—— 程序員 A: 好的。

NoBug.java

package ceshi;
import ceshi.Jiecha;


public class NoBug {

    @Jiecha
    public void suanShu(){
        System.out.println("1234567890");
    }
    @Jiecha
    public void jiafa(){
        System.out.println("1+1="+1+1);
    }
    @Jiecha
    public void jiefa(){
        System.out.println("1-1="+(1-1));
    }
    @Jiecha
    public void chengfa(){
        System.out.println("3 x 5="+ 3*5);
    }
    @Jiecha
    public void chufa(){
        System.out.println("6 / 0="+ 6 / 0);
    }

    public void ziwojieshao(){
        System.out.println("我寫的程序沒有 bug!");
    }

}

上面的代碼,有些方法上面運用了 @Jiecha 註解。

package ceshi;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface Jiecha {

}

 再編寫一個測試類 TestTool 就可以測試 NoBug 相應的方法了

package ceshi;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;



public class TestTool {

    public static void main(String[] args) {
        // TODO Auto-generated method stub

        NoBug testobj = new NoBug();

        Class clazz = testobj.getClass();

        Method[] method = clazz.getDeclaredMethods();
        //用來記錄測試產生的 log 信息
        StringBuilder log = new StringBuilder();
        // 記錄異常的次數
        int errornum = 0;

        for ( Method m: method ) {
            // 只有被 @Jiecha 標註過的方法才進行測試
            if ( m.isAnnotationPresent( Jiecha.class )) {
                try {
                    m.setAccessible(true);
                    m.invoke(testobj, null);

                } catch (Exception e) {
                    // TODO Auto-generated catch block
                    //e.printStackTrace();
                    errornum++;
                    log.append(m.getName());
                    log.append(" ");
                    log.append("has error:");
                    log.append("\n\r  caused by ");
                    //記錄測試過程中,發生的異常的名稱
                    log.append(e.getCause().getClass().getSimpleName());
                    log.append("\n\r");
                    //記錄測試過程中,發生的異常的具體信息
                    log.append(e.getCause().getMessage());
                    log.append("\n\r");
                } 
            }
        }


        log.append(clazz.getSimpleName());
        log.append(" has  ");
        log.append(errornum);
        log.append(" error.");

        // 生成測試報告
        System.out.println(log.toString());

    }

}

測試的結果是: 

1234567890
1+1=11
1-1=0
3 x 5=15
chufa has error:

  caused by ArithmeticException

/ by zero

NoBug has  1 error.

提示 NoBug 類中的 chufa() 這個方法有異常,這個異常名稱叫做 ArithmeticException,原因是運算過程中進行了除 0 的操作。

所以,NoBug 這個類有 Bug。

通過上述註解完成了自己的目的,即對別人的代碼進行測試。

 

總結

  1. 如果註解難於理解,你就把它類同於標籤,標籤爲了解釋事物,註解爲了解釋代碼。
  2. 註解的基本語法,創建如同接口,但是多了個 @ 符號。
  3. 註解的元註解。
  4. 註解的屬性。
  5. 註解主要給編譯器及工具類型的軟件用的。
  6. 註解的提取需要藉助於 Java 的反射技術,反射比較慢,所以註解使用時也需要謹慎計較時間成本。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章