Java 註解完全解析

maoqitian發表於2019-04-29

關於註解首先引入官方文件的一句話:Java 註解用於為 Java 程式碼提供後設資料。作為後設資料,註解不直接影響你的程式碼執行,但也有一些型別的註解實際上可以用於這一目的。Java 註解是從 Java5 開始新增到 Java 的。看完這句話也許你還是一臉懵逼,接下我將從註解的定義、元註解、註解屬性、自定義註解、註解解析JDK 提供的註解這幾個方面再次瞭解註解(Annotation)

註解的定義

  • 日常開發中新建Java類,我們使用class、interface比較多,而註解和它們一樣,也是一種類的型別,他是用的修飾符為 @interface

Java中新建類

註解類的寫法

  • 我們新建一個註解MyTestAnnotation
public @interface MyTestAnnotation {

}
複製程式碼
  • 接著我們就可以在類或者方法上作用我們剛剛新建的註解
@MyTestAnnotation
public class test {
   @MyTestAnnotation
   public static void main(String[] args){
   }
}
複製程式碼
  • 以上我們只是瞭解了註解的寫法,但是我們定義的註解中還沒寫任何程式碼,現在這個註解毫無意義,要如何使註解工作呢?接下來我們接著瞭解元註解。

元註解

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

@Retention

  • Retention英文意思有保留、保持的意思,它表示註解存在階段是保留在原始碼(編譯期),位元組碼(類載入)或者執行期(JVM中執行)。在@Retention註解中使用列舉RetentionPolicy來表示註解保留時期
  • @Retention(RetentionPolicy.SOURCE),註解僅存在於原始碼中,在class位元組碼檔案中不包含
  • @Retention(RetentionPolicy.CLASS), 預設的保留策略,註解會在class位元組碼檔案中存在,但執行時無法獲得
  • @Retention(RetentionPolicy.RUNTIME), 註解會在class位元組碼檔案中存在,在執行時可以通過反射獲取到
  • 如果我們是自定義註解,則通過前面分析,我們自定義註解如果只存著原始碼中或者位元組碼檔案中就無法發揮作用,而在執行期間能獲取到註解才能實現我們目的,所以自定義註解中肯定是使用 @Retention(RetentionPolicy.RUNTIME)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTestAnnotation {

}
複製程式碼

@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型別
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyTestAnnotation {

}
複製程式碼

@Documented

  • Document的英文意思是文件。它的作用是能夠將註解中的元素包含到 Javadoc 中去。

@Inherited

  • Inherited的英文意思是繼承,但是這個繼承和我們平時理解的繼承大同小異,一個被@Inherited註解了的註解修飾了一個父類,如果他的子類沒有被其他註解修飾,則它的子類也繼承了父類的註解。
  • 下面我們來看個@Inherited註解例子
/**自定義註解*/
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyTestAnnotation {
}
/**父類標註自定義註解*/
@MyTestAnnotation
public class Father {
}
/**子類*/
public class Son extends Father {
}
/**測試子類獲取父類自定義註解*/
public class test {
   public static void main(String[] args){

      //獲取Son的class物件
       Class<Son> sonClass = Son.class;
      // 獲取Son類上的註解MyTestAnnotation可以執行成功
      MyTestAnnotation annotation = sonClass.getAnnotation(MyTestAnnotation.class);
   }
}
複製程式碼

@Repeatable

  • Repeatable的英文意思是可重複的。顧名思義說明被這個元註解修飾的註解可以同時作用一個物件多次,但是每次作用註解又可以代表不同的含義。
  • 下面我們看一個人玩遊戲的例子
/**一個人喜歡玩遊戲,他喜歡玩英雄聯盟,絕地求生,極品飛車,塵埃4等,則我們需要定義一個人的註解,他屬性代表喜歡玩遊戲集合,一個遊戲註解,遊戲屬性代表遊戲名稱*/
/**玩家註解*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface People {
    Game[] value() ;
}
/**遊戲註解*/
@Repeatable(People.class)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Game {
    String value() default "";
}
/**玩遊戲類*/
@Game(value = "LOL")
@Game(value = "PUBG")
@Game(value = "NFS")
@Game(value = "Dirt4")
public class PlayGame {
}
複製程式碼
  • 通過上面的例子,你可能會有一個疑問,遊戲註解中括號的變數是啥,其實這和遊戲註解中定義的屬性對應。接下來我們繼續學習註解的屬性。

註解的屬性

  • 通過上一小節@Repeatable註解的例子,我們說到註解的屬性。註解的屬性其實和類中定義的變數有異曲同工之處,只是註解中的變數都是成員變數(屬性),並且註解中是沒有方法的,只有成員變數,變數名就是使用註解括號中對應的引數名,變數返回值註解括號中對應引數型別。相信這會你應該會對上面的例子有一個更深的認識。而@Repeatable註解中的變數則型別則是對應Annotation(介面)的泛型Class。
/**註解Repeatable原始碼*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Repeatable {
    /**
     * Indicates the <em>containing annotation type</em> for the
     * repeatable annotation type.
     * @return the containing annotation type
     */
    Class<? extends Annotation> value();
}
複製程式碼

註解的本質

  • 註解的本質就是一個Annotation介面
/**Annotation介面原始碼*/
public interface Annotation {
   
    boolean equals(Object obj);

    int hashCode();
    
    Class<? extends Annotation> annotationType();
}
複製程式碼
  • 通過以上原始碼,我們知道註解本身就是Annotation介面的子介面,也就是說註解中其實是可以有屬性和方法,但是介面中的屬性都是static final的,對於註解來說沒什麼意義,而我們定義介面的方法就相當於註解的屬性,也就對應了前面說的為什麼註解只有屬性成員變數,其實他就是介面的方法,這就是為什麼成員變數會有括號,不同於介面我們可以在註解的括號中給成員變數賦值。

註解屬性型別

  • 註解屬性型別可以有以下列出的型別
  • 1.基本資料型別
  • 2.String
  • 3.列舉型別
  • 4.註解型別
  • 5.Class型別
  • 6.以上型別的一維陣列型別

註解成員變數賦值

  • 如果註解又多個屬性,則可以在註解括號中用“,”號隔開分別給對應的屬性賦值,如下例子,註解在父類中賦值屬性
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MyTestAnnotation {
    String name() default "mao";
    int age() default 18;
}

@MyTestAnnotation(name = "father",age = 50)
public class Father {
}
複製程式碼

獲取註解屬性

  • 前面我們說了很多註解如何定義,放在哪,現在我們可以開始學習註解屬性的提取了,這才是使用註解的關鍵,獲取屬性的值才是使用註解的目的。
  • 如果獲取註解屬性,當然是反射啦,主要有三個基本的方法
 /**是否存在對應 Annotation 物件*/
  public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {
        return GenericDeclaration.super.isAnnotationPresent(annotationClass);
    }
  
 /**獲取 Annotation 物件*/
    public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {
        Objects.requireNonNull(annotationClass);

        return (A) annotationData().annotations.get(annotationClass);
    }
 /**獲取所有 Annotation 物件陣列*/   
 public Annotation[] getAnnotations() {
        return AnnotationParser.toArray(annotationData().annotations);
    }    
複製程式碼
  • 下面結合前面的例子,我們來獲取一下註解屬性,在獲取之前我們自定義的註解必須使用元註解@Retention(RetentionPolicy.RUNTIME)
public class test {
   public static void main(String[] args) throws NoSuchMethodException {

        /**
         * 獲取類註解屬性
         */
        Class<Father> fatherClass = Father.class;
        boolean annotationPresent = fatherClass.isAnnotationPresent(MyTestAnnotation.class);
        if(annotationPresent){
            MyTestAnnotation annotation = fatherClass.getAnnotation(MyTestAnnotation.class);
            System.out.println(annotation.name());
            System.out.println(annotation.age());
        }

        /**
         * 獲取方法註解屬性
         */
        try {
            Field age = fatherClass.getDeclaredField("age");
            boolean annotationPresent1 = age.isAnnotationPresent(Age.class);
            if(annotationPresent1){
                Age annotation = age.getAnnotation(Age.class);
                System.out.println(annotation.value());
            }

            Method play = PlayGame.class.getDeclaredMethod("play");
            if (play!=null){
                People annotation2 = play.getAnnotation(People.class);
                Game[] value = annotation2.value();
                for (Game game : value) {
                    System.out.println(game.value());
                }
            }
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }
}
複製程式碼

執行結果:

獲取註解屬性執行結果

JDK 提供的註解

註解 作用 注意事項
@Override 它是用來描述當前方法是一個重寫的方法,在編譯階段對方法進行檢查 jdk1.5中它只能描述繼承中的重寫,jdk1.6中它可以描述介面實現的重寫,也能描述類的繼承的重寫
@Deprecated 它是用於描述當前方法是一個過時的方法
@SuppressWarnings 對程式中的警告去除。

註解作用與應用

  • 現在我們再次回頭看看開頭官方文件的那句描述

Java 註解用於為 Java 程式碼提供後設資料。作為後設資料,註解不直接影響你的程式碼執行,但也有一些型別的註解實際上可以用於這一目的。

  • 經過我們前面的瞭解,註解其實是個很方便的東西,它存活的時間,作用的區域都可以由你方便設定,只是你用註解來幹嘛的問題

使用註解進行引數配置

  • 下面我們看一個銀行轉賬的例子,假設銀行有個轉賬業務,轉賬的限額可能會根據匯率的變化而變化,我們可以利用註解靈活配置轉賬的限額,而不用每次都去修改我們的業務程式碼。
/**定義限額註解*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface BankTransferMoney {
    double maxMoney() default 10000;
}
/**轉賬處理業務類*/
public class BankService {
    /**
     * @param money 轉賬金額
     */
    @BankTransferMoney(maxMoney = 15000)
    public static void TransferMoney(double money){
        System.out.println(processAnnotationMoney(money));

    }
    private static String processAnnotationMoney(double money) {
        try {
            Method transferMoney = BankService.class.getDeclaredMethod("TransferMoney",double.class);
            boolean annotationPresent = transferMoney.isAnnotationPresent(BankTransferMoney.class);
            if(annotationPresent){
                BankTransferMoney annotation = transferMoney.getAnnotation(BankTransferMoney.class);
                double l = annotation.maxMoney();
                if(money>l){
                   return "轉賬金額大於限額,轉賬失敗";
                }else {
                    return"轉賬金額為:"+money+",轉賬成功";
                }
            }
        } catch ( NoSuchMethodException e) {
            e.printStackTrace();
        }
        return "轉賬處理失敗";
    }
    public static void main(String[] args){
        TransferMoney(10000);
    }
}
複製程式碼

執行結果:

轉賬處理執行結果

  • 通過上面的例子,只要匯率變化,我們就改變註解的配置值就可以直接改變當前最大限額。

第三方框架的應用

  • 作為一個Android 開發者,平常我們所使用的第三方框架ButterKnife,Retrofit2,Dagger2等都有註解的應用,如果我們要了解這些框架的原理,則註解的基礎知識則是必不可少的。

註解的作用

  • 提供資訊給編譯器: 編譯器可以利用註解來檢測出錯誤或者警告資訊,列印出日誌。
  • 編譯階段時的處理: 軟體工具可以用來利用註解資訊來自動生成程式碼、文件或者做其它相應的自動處理。
  • 執行時處理: 某些註解可以在程式執行的時候接受程式碼的提取,自動做相應的操作。
  • 正如官方文件的那句話所說,註解能夠提供後設資料,轉賬例子中處理獲取註解值的過程是我們開發者直接寫的註解提取邏輯,處理提取和處理 Annotation 的程式碼統稱為 APT(Annotation Processing Tool)。上面轉賬例子中的processAnnotationMoney方法就可以理解為APT工具類。

最後說點

到此,對於Java中註解的解析就結束了。最後,也非常感謝您閱讀我的文章,文章中如果有錯誤,請大家給我提出來,大家一起學習進步,如果覺得我的文章給予你幫助,也請給我一個喜歡和關注,同時也歡迎訪問我的個人部落格

相關文章