1. 程式人生 > 程式設計 >Java SE基礎鞏固(九):註解

Java SE基礎鞏固(九):註解

官方檔案是這麼描述註解的:

Annotations,a form of metadata,provide data about a program that is not part of the program itself. Annotations have no direct effect on the operation of the code they annotate.

Annotations have a number of uses,among them:

  • Information for the compiler — Annotations can be used by the compiler to detect errors or suppress warnings.
  • Compile-time and deployment-time processing — Software tools can process annotation information to generate code,XML files,and so forth.
  • Runtime processing — Some annotations are available to be examined at runtime.

簡單翻譯一下:註解是一種元資料,提供和程式相關的資料但不是程式本身的一部分,對他們註解的的程式碼沒有直接影響。主要有幾種用途:為編譯器提供資訊、編譯時和部署時處理,執行時處理。

說實話,要第一次接觸註解,看到這樣的解釋,肯定是雲裡霧裡的(天才請忽略),這他丫在說啥?元資料是啥?為什麼能提供和程式有關的程式,又不是程式本身的一部分?.....

不如換一個思路,直接把註解當做標籤,標籤都知道吧,就是描述一種事物的東西,例如圖書館的書都貼有小條,該小條就是標籤,小條有不同的顏色,形狀,內容,這些就是標籤的屬性。從這個角度出發,我們“重新定義”註解:註解是一種用來描述程式的標籤,對程式本身沒有直接影響,換句話說,即使給一本書貼上了標籤,也不會對書本身的內容有直接影響,書還是那本書。

接下來我講介紹一些Java註解相關的內容,包括

  • 如何使用註解
  • 如何自定義註解
  • 元註解
  • 註解和反射結合

1 如何使用註解

註解可以作用在類、方法、欄位、介面甚至是註解上(還有其他,後面會列出一個完整的列表),具體取決於註解是如何定義的。假設現在有有個@Yeonon註解,他可以作用類、方法、欄位上,那我們可以寫出這樣的程式:

@Yeonon
public class Main {

    @Yeonon
    private String name;
    
    @Yeonon
    public void testMethod1() {
        System.out.println("test method 1");
    }
    
    public static void main(String[] args) {
        
    }
}
複製程式碼

使用起來就是那麼簡單,如果該註解有屬性,還可以對屬性進行設定,為編譯器或者執行時處理程式提供更多的資訊,例如:

@Yeonon(value = "Main")
public class Main {

    @Yeonon(value = "name")
    private String name;

    @Yeonon(value = "testMethod1")
    public void testMethod1() {
        System.out.println("test method 1");
    }


    public static void main(String[] args) {

    }
}
複製程式碼

關於如何使用就講到這吧,下面來介紹一下如何定義註解以及什麼是元註解。

2 自定義註解以及元註解

註解通過@interface語法定義,如下所示:

public @interface Yeonon {
}
複製程式碼

但光這樣定義註解,註解是無法正常工作的,他沒有指明該註解可作用的元素型別,也沒有指明註解的作用時間範圍(即該註解在什麼時候是生效的,什麼時候是無效的),那如何指明呢?答案是使用元註解

2.1 元註解

元註解即作用在註解上的註解,用來描述註解。如果把註解看做標籤,那麼元註解就是描述某個標籤的標籤,本質上仍然是一個標籤,只是他描述的物件是標籤,而普通標籤描述的是除標籤以外的事物。還是有點繞,直接來看程式碼吧:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.TYPE,ElementType.METHOD})
public @interface Yeonon {
}

複製程式碼

程式碼中的@Retention和@Target就是所謂的元註解,他們作用在註解上,更準確的說法是作用在註解定義上。之所以這樣說,是因為可能會有朋友將下面程式碼所示的註解當做是元註解:

@Value
@Yeonon
private string name;
複製程式碼

這裡@Value和@Yeonon都不是元註解,只是兩個註解同時作用在一個欄位上而已,簡單理解就是一本書有兩個標籤。

Java中內建了5中元註解,分別是: @Retention、@Documented、@Target、@Inherited、@Repeatable 。

2.1.1 @Retention

Retention翻譯過來就是保留期的意思,@Retention就是用來描述註解的保留時間的,具體的保留時間根據其value屬性來確定,其value屬性是RetentionPolicy型別的值,該型別有如下幾種取值:

  • RetentionPolicy.SOURCE,保留到原始碼期,編譯的時候會將其忽略。
  • RetentionPolicy.CLASS,保留到編譯期,執行時會將其忽略。
  • RetentionPolicy.RUNTIME,保留到執行時,執行時可以獲取到。

2.1.2 @Documented

Documented翻譯過來就是檔案的意思。作用是將註解的元素包含到Java doc中。

2.1.3 @Target

Target翻譯過來是目標的意思,@Target的作用是指定該註解作用的地方,例如方法、欄位、介面。可以有如下取值:

  • ElementType.TYPE,即作用在類和介面上。
  • ElementType.FIELD,即作用在欄位上。
  • ElementType.METHOD,即作用在方法上。
  • ElementType.PARAMETER,即作用在引數上。
  • ElementType.CONSTRUCTOR,即作用在構造器上。
  • ElementType.LOCAL_VARIABLE,即作用在本地變數上。
  • ElementType.ANNOTATION_TYPE,即作用在註解上。
  • ElementType.PACKAGE,即作用在包上,從名字上好像是這樣的,但沒看到過哪裡有這樣使用的。
  • ElementType.TYPE_PARAMETER,即作用在型別引數上,Java8新增的。
  • ElementType.TYPE_USE,Java8新增的,沒搞懂是什麼。

2.1.4 @Inherited

Inherited翻譯過來是繼承的意思,但並不是指註解可以被繼承,而是指的如果一個類被@Inherited註解作用的註解進行註解,那麼其子類也會被該註解作用。有點繞,直接來看程式碼吧:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.METHOD})
@Inherited
public @interface Yeonon {
}

@Yeonon
public class Base {
    //...
}

public class Sub extends Base {
    //...
}
複製程式碼

@Yeonon上有@Inherited註解,然後@Yeonon作用到Base類上,而Sub類是Base類的子類,那麼Sub類預設就也有@Yeonon註解。

2.1.5 @Repeatable

Repeatable翻譯過來是可重複的意思,這是Java8新增的元註解,那這個註解有什麼用呢?先來看一個場景,假設一個人既是程式設計師、又是產品經理(舉個例子而已,哈哈)、又是老闆,現在有一個@Identity註解,如下所示:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.METHOD,ElementType.TYPE})
public @interface Identity {
    String role();
}

複製程式碼

可能會這樣使用註解來描述一個人:

@Identity(role = "coder")
@Identity(role = "pm")
private User user;
複製程式碼

編譯一下,發現無法通過編譯,錯誤提示大致是,該註解型別是不可重複的(Java8)。在Java7之前,可能就會定義一個新的可以容納多個元素的註解來解決這個問題,如下所示:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.TYPE})
public @interface Identites {
    Identity[] value();
}

@Identites(roles = {@Identity(role = "coder"),@Identity(role = "pm")})
private User user;

複製程式碼

這樣做可以解決問題,但可讀性並不好,而且會給註解處理程式帶來麻煩。在Java8中,可以使用@Repeatable元註解來表示註解可以重複使用,如下所示:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.TYPE})
@Repeatable(value = Identites.class)
public @interface Identity {
    String role();
}

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.TYPE})
public @interface Identites {
    Identity[] value();
}

//使用
@Identity(role = "coder")
@Identity(role = "pm")
@Identity(role = "boss")
private User user;

複製程式碼

可讀性好了很多,我們看一眼就知道這個人有三個身份,coder,pm和boss。但仍然需要一個新的註解(例子中的@Identites)來容納多個元素,這種型別的註解被稱作“容器註解”。

除了上述的5個內建的元註解,實際上我們還可以自定義元註解,還記得之前講@Target註解的時候,ElementType型別有一個ANNOTATION_TYPE屬性嗎?在@Target的value屬性的集合中加入這個型別,就表示該註解是一個元註解了,但最好不用過度使用該功能,因為可能會導致一些邏輯混亂。

2.2 屬性

在上面的程式碼中,其實已經出現過屬性了,例如之前定義的@Identity註解,該註解有一個String型別的屬性,名字是role,如下所示:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.TYPE})
@Repeatable(value = Identites.class)
public @interface Identity {
    String role();
}
複製程式碼

發現這個和宣告類的欄位有些不一樣,比宣告欄位多了一個括號。這是註解的語法,至於為什麼非要這樣搞,我也不太明白。屬性的型別可以是8中基本型別即其陣列型別、引用型別和註解型別,如果宣告屬性的時候,沒有default值,在使用註解的時候就必須給該屬性賦值。例如上面的role屬性,因為沒有預設值,所以在使用的時候必須給出role的值,那預設值該如何定義呢?直接來看程式碼:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD,ElementType.TYPE})
@Repeatable(value = Identites.class)
public @interface Identity {
    String role();
    
    String name() default "Identity";
}
複製程式碼

即在屬性宣告之後加入default關鍵字和預設值,非常簡單。

那這些屬性有什麼用呢?可以這樣簡單的理解:屬性提供了額外的資訊。舉個例子,如果@Identity沒有屬性(這種註解稱作標記註解),當他作用在某個地方的時候,程式包括我們人類都僅僅能知道該註解有一個身份,但不知道具體是什麼身份,為了讓程式和人類能知道具體是什麼身份,就需要用到屬性了,例如上述的role屬性,此時再使用@Identity的時候,就可以新增role屬性的值,用來表示具體的身份,例如coder,pm等。

下面來介紹一下註解和反射的結合,如果這裡對屬性還是有些不明白也沒關係,下面的介紹會加深對屬性的理解。

3 註解和反射結合

上面講了那麼多,你是否有一個疑問:程式是如何從這些註解中提取資訊的?答案就是結合反射(關於反射,我在之前的文章有說過,在此就直接使用了,不再講原理),通過反射獲取到類、欄位、方法上的註解,然後對註解進行解析並作出相應的處理。下面我將通過一個簡單的測試框架來演示註解如何和反射結合使用。

首先,先編寫一個註解,當某個方法上有該註解時,就表示該方法應該被測試執行,如下所示:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface YTest {
    //暫時不需要屬性
}

複製程式碼

然後,開始編寫測試框架:

public class MyTestTool {

    public static void main(String[] args) throws ClassNotFoundException {
        //獲取待測試類的類物件
        Class<?> testClass = Class.forName("top.yeonon.anotation.MyTest");
        int passed = 0; //通過測試的數量
        int tested = 0; //測試的數量

        //獲取待測試類所有方法
        Method[] methods = testClass.getDeclaredMethods();

        for (Method method : methods) {
            //如果還方法上有YTest註解,就對其做處理,否則就直接跳過
            if (method.isAnnotationPresent(YTest.class)) {
                tested++;
                try {
                    method.invoke(null);
                    passed++; //能走到這,說明沒有發生異常,即測試通過
                } catch (InvocationTargetException e) {
                    System.out.println(method.getName() + " test failed!");
                } catch (Exception e) {
                    System.out.println("Invalid test : " + method.getName());
                }
            }
        }

        System.out.println("tested : " + tested);
        System.out.println("Passed : " + passed);
        System.out.println("Failed : " + (tested - passed));
        System.out.println("Passed Rate : " + ((double)passed / (double)tested));
    }
}
複製程式碼

該測試框架非常簡單粗暴,最核心的邏輯是通過反射來判斷方法上是否有@YTest註解,如果沒有,直接跳過該方法,不計入測試總數裡,如果有,就呼叫invoke()方法執行該方法,因為該框架只對靜態方法做測試,所以invoke方法的引數是null。如果丟擲異常則表示失敗(實際上真正的測試框架不會那麼簡單),如果沒有丟擲異常則表示成功,最後列印輸出一些資訊作為結果報告。

最後,編寫待測試類,如下所示:

public class MyTest {

    @YTest
    public void m1() {
        //do something
    }

    @YTest
    public void m2() {
        //丟擲異常來表示測試失敗
        throw new RuntimeException();
    }

    public void m3() {

    }

    @YTest
    public static void m4() {

    }

    @YTest
    public static void m5() {
        throw new RuntimeException();
    }

    public static void m6() {

    }
}
複製程式碼

一共有6個方法,3個例項方法,3個靜態方法,只有4個方法有@YTest註解,其中有兩個方法丟擲異常,分別是m2和m5。先來來執行一下之前寫的測試框架程式吧,執行結果大致如下所示:

m5 test failed!
Invalid test : m1
Invalid test : m2
tested : 4
Passed : 1
Failed : 3
Passed Rate : 0.25
複製程式碼

可以看到,m5測試失敗,因為m5丟擲了異常,m1和m2則是非法測試,因為m1和m2是例項方法,即使m2內部也丟擲了異常,但實際上再執行之前就已經出現了InvocationTargetException,該異常先發生,根本不會呼叫m2。最後幾行表示共有4個測試用例,通過了1個,失敗了3個,通過率是25%。

一個小型的測試框架就算是完成了,雖然簡單粗糙,但作為演示反射和註解的結合已經完全足夠了,相信有了上面的介紹,對於如何將反射和註解結合在一起,你已經大概明白了。

4 小結

本文簡單介紹了什麼是註解、元註解、註解的使用以及反射和註解結合使用。註解是Java5提供的一個強大的特性,很多框架例如JUnit4、Spring家族的產品例如Spring Boot,Spring Cloud系列都大量的使用註解來簡化程式設計,可見註解的功能是多麼強大,而且Java8中還新增了很多和註解有關的東西,從這也可以看出,Java官方也在大量發展註解。