1. 程式人生 > 實用技巧 >介面向下轉型,子類方法安全問題

介面向下轉型,子類方法安全問題

目錄

1. 什麼是註解?

註解(Annotation),也叫元資料。一種程式碼級別的說明。它是JDK1.5及以後版本引入的一個特性,與類、介面、列舉是在同一個層次。它可以宣告在包、類、欄位、方法、區域性變數、方法引數等的前面,用來對這些元素進行說明,註釋。

2. 註解的由來

以前,XML是各大框架的青睞者,它以鬆耦合的方式完成了框架中幾乎所有的配置,但是隨著專案越來越龐大,XML

的內容也越來越複雜,維護成本變高。

於是就有人提出來一種標記式高耦合的配置方式,註解。方法上可以進行註解,類上也可以註解,欄位屬性上也可以註解,反正幾乎需要配置的地方都可以進行註解。

關於註解XML兩種不同的配置模式,爭論了好多年了,各有各的優劣,註解可以提供更大的便捷性,易於維護修改,但耦合度高,而 XML 相對於註解則是相反的。

3. JDK內建的註解

Java 定義了一套註解,共有 7 個,3 個在 java.lang 中,剩下 4 個在 java.lang.annotation 中。

作用在程式碼的註解是

  • @Override : 檢查該方法是否是重寫方法。如果發現其父類,或者是引用的介面中並沒有該方法時,會報編譯錯誤。
  • @Deprecated : 標記過時方法。如果使用該方法,會報編譯警告。
  • @SuppressWarnings :指示編譯器去忽略註解中宣告的警告。

作用在其他註解的註解(或者說 元註解)是:

  • @Retention :標識這個註解怎麼儲存,是隻在程式碼中,還是編入class檔案中,或者是在執行時可以通過反射訪問。
  • @Documented :標記這些註解是否包含在使用者文件中。
  • @Target : 標記這個註解應該是哪種 Java 成員。
  • @Inherited : 標記這個註解是繼承於哪個註解類(預設 註解並沒有繼承於任何子類)

從 Java 7 開始,額外添加了 3 個註解:

  • @SafeVarargs :Java 7 開始支援,忽略任何使用引數為泛型變數的方法或建構函式呼叫產生的警告。
  • @FunctionalInterface :Java 8 開始支援,標識一個匿名函式或函式式介面。
  • @Repeatable :Java 8 開始支援,標識某註解可以在同一個宣告上使用多次。

4. 元註解解析

元註解,我們可以理解為註解的註解,它是作用在註解中,方便我們使用註解實現想要的功能。元註解分別有@Retention、 @Target、 @Document、 @Inherited和@Repeatable(JDK1.8加入)五種。

4.1 @Retention

Retention英文意思為保留,它定義了註解被保留的時間長短,分為編譯期、類載入時期和執行時期。

@Retention註解中使用列舉RetentionPolicy來表示註解保留時期:

  • @Retention(RetentionPolicy.SOURCE):註解僅存在於原始碼中(即原始檔保留)
  • @Retention(RetentionPolicy.CLASS):預設的保留策略,註解會在class位元組碼檔案中存在,但執行時無法獲得(即class保留)
  • @Retention(RetentionPolicy.RUNTIME): 註解會在class位元組碼檔案中存在,在執行時可以通過反射獲取到(即執行時保留)

以SpringBoot啟動類的@SpringBootApplication註解為例:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface SpringBootApplication {
    //...
}

@SpringBootApplication註解的的RetentionPolicy的屬性值是RUTIME,這樣註解處理器可以通過反射,獲取到該註解的屬性值,從而去做一些執行時的邏輯處理。

4.2 @Target

Target的英文意思為目標,它定義了註解作用的範圍,可以作用在類、方法、變數、列舉等等。

@Target通過列舉類ElementType表達作用的範圍:

  • @Target(ElementType.TYPE) :作用在介面、類、列舉、註解
  • @Target(ElementType.FIELD) :作用在屬性欄位、列舉的常量
  • @Target(ElementType.METHOD): 作用在方法
  • @Target(ElementType.PARAMETER) :作用在方法引數
  • @Target(ElementType.CONSTRUCTOR) :作用在建構函式
  • @Target(ElementType.LOCAL_VARIABLE):作用在區域性變數
  • @Target(ElementType.ANNOTATION_TYPE):作用於註解(@Retention註解中就使用該屬性)
  • @Target(ElementType.PACKAGE) :作用於包
  • @Target(ElementType.TYPE_PARAMETER) :作用於泛型,即泛型方法、泛型類、泛型介面 (jdk1.8加入)
  • @Target(ElementType.TYPE_USE) :型別使用.可以用於標註任意型別除了 Class (jdk1.8加入)

我們一般比較常用的是ElementType.TYPE型別。

4.3 @Documented

Documented顧名思義,它的作用是能夠將註解中的元素包含到 Javadoc (API文件)中去。

4.4 @Inherited

Inherited的英文意思是繼承,它表示,當一個父類被@Inherited註解修飾,那麼它的子類可以繼承父類的註解。

4.5 Repeatable

Repeatable的英文意思是可重複的,它說明被這個元註解修飾的註解可以同時作用一個物件多次,但是每次作用註解又可以代表不同的含義。

5. 自定義註解並使用

基於前面的學習,我們對註解有了一個基本的認識:它是JDK1.5及以後版本引入的一個特性,與類、介面、列舉是在同一個層次。它可以宣告在包、類、欄位、方法、區域性變數、方法引數等的前面,用來對這些元素進行說明,註釋。,然後程式在編譯時或執行時可以檢測到這些註解從而執行一些特殊操作。

因此可以得出自定義註解使用的基本流程:

  • 第一步,定義註解——相當於定義標記;
  • 第二步,配置註解——把標記打在需要用到的程式程式碼中;
  • 第三步,解析註解——在編譯期或執行時檢測到標記,並進行特殊操作。

5.1 定義註解

註解在Java中,與類、介面、列舉類似,因此其宣告語法基本一致,只是所使用的關鍵字有所不同:@interface在底層實現上,所有定義的註解都會自動繼承java.lang.annotation.Annotation介面

public @interface MyAnnotation {
}

根據我們在自定義類的經驗,在類的實現部分無非就是書寫構造、屬性或方法。但是,在自定義註解中,其實現部分只能定義一個東西:註解型別元素(annotation type element)

什麼是註解型別元素呢?Java做了如下定義:

  • 所有基本型別(int,float,boolean,byte,double,char,long,short)
  • String
  • Class
  • enum
  • Annotation
  • 上述型別的一維陣列

倘若使用了其他資料型別,編譯器將會丟擲一個編譯錯誤。

我們來實現一下宣告好的@MyAnnotation註解:

public @interface MyAnnotation {
    public String name();
	int age() default 20;
	int[] array();
}

注意:

  1. 訪問修飾符必須為public,不寫預設為public;
  2. 該元素的名稱一般定義為名詞,如果註解中只有一個元素,請把名字起為value(後面使用會帶來便利操作);
  3. ()不是定義方法引數的地方,也不能在括號中定義任何引數,僅僅只是一個特殊的語法;
  4. default代表預設值,值必須和第2點定義的型別一致;
  5. 如果沒有預設值,代表後續使用註解時必須給該型別元素賦值。

根據前面對元註解的學習,我們給@MyAnnotation做一下修飾:

@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD})
public @interface MyAnnotation {
    public String name();
	int age() default 20;
	int[] array();
}

上面元註解的含義為:@MyAnnotation能保留到執行期、只能作用在方法上。

5.2 配置註解

定義好註解之後,我們就可以在類上配置了,新增一個測試類:

public class Person{
    public void show(Integer times){
        for(int i = 0; i < times; i++){
            System.out.println("person show!");
        }    
    }
}

由於我們在@MyAnnotation中定義的有註解型別元素,而且有些元素是沒有預設值的,這要求我們在使用的時候必須在@MyAnnotation後面打上(),並且在()內以“元素名=元素值“的形式挨個填上所有沒有預設值的註解型別元素(有預設值的也可以填上重新賦值),中間用“,”號分割。

所以註解的配置形式如下:

public class Person{
    @MyAnnotation(name = "tom",age = 20,score = {59,68,88})
    public void show(Integer times){
        for(int i = 0; i < times; i++){
            System.out.println("person show!");
        }    
    }
}

注意事項:

  1. 如果註解本身沒有註解型別元素,那麼在使用註解的時候可以省略(),直接寫為:@註解名,它和標準語法@註解名()等效!
  2. 如果註解本本身只有一個註解型別元素,而且命名為value,那麼在使用註解的時候可以直接使用:@註解名(註解值),其等效於:@註解名(value = 註解值)
  3. 如果註解中的某個註解型別元素是一個數組型別,在使用時又出現只需要填入一個值的情況,那麼在使用註解時可以直接寫為:@註解名(型別名 = 型別值),它和標準寫法:@註解名(型別名 = {型別值})等效!
  4. 如果一個註解的@Target是定義為Element.PACKAGE,那麼這個註解是配置在package-info.java中的,而不能直接在某個類的package程式碼上面配置。

5.3 反射獲取註解

我們定義的@MyAnnotation的保留時期是執行期,所以我們可以使用反射來獲取註解。

示例程式碼:

public class TestAnnotation {
    public static void main(String[] args){
        try {
            //獲取Person的Class物件
            Class personClass = Class.forName("com.lfx.test.Person");

            //這裡形參寫成Integer.class
            Method personMethod = personClass.getMethod("show",Integer.class);

            if(personMethod.isAnnotationPresent(MyAnnotation.class)){
                System.out.println("Person類上配置了MyAnnotation註解!");
                //獲取該元素上指定型別的註解
                MyAnnotation myAnnotation = personMethod.getAnnotation(MyAnnotation.class);
                System.out.println("name: " + myAnnotation.name() + ", age: " + myAnnotation.age()
                    + ", score: " + myAnnotation.score()[0]);
            }else{
                System.out.println("Person類上沒有配置MyAnnotation註解!");
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }
}

獲取在其他作用範圍的註解的方法,已在反射一章做詳細介紹,這裡不再多言。