第 17 章 Annotation

J2SE 5.0 中對 metadata 提出的功能是 Annotation,metadata 就是「資料的資料」(Data about data),突然看到這樣的解釋會覺得奇怪,但以表格為例,表格中呈現的就是資料,但有時候還會有額外的資料用來說明表格的作用,從這個角度來看,metadata 就不這麼的奇怪。

在 J2SE 5.0 中,Annotation 的主要目的介於原始碼與 API 文件說明之間,Annotation 對程式碼作出一些說明與解釋,Class 中可以包含這些解釋,編譯器或其它程式分析工作可以使用 Annotation 來作分析,您可以從 java.lang.Override、java.lang.Deprecated、java.lang.SuppressWarnings 這三個 J2SE 5.0 中標準的 Annotation 型態開始瞭解 Annotation 的作用。


17.1 Annotation

Annotation 對程式運行沒有影響,它的目的在對編譯器或分析工具說明程式的某些資訊,您可以在套件、類別、方法、資料成員等加上 Annotation,每一個 Annotation 對應於一個實際的 Annotation 型態,您可以從 java.lang.Override、java.lang.Deprecated、java.lang.SuppressWarnings 這三個 J2SE 5.0 中標準的 Annotation 型態開始瞭解 Annotation 的作用,這個小節也將告訴您如何自訂 Annotation 型態。

17.1.1 限定 Override 父類方法 @Override

java.lang.Override 是 J2SE 5.0 中標準的 Annotation 型態之一,它對編譯器說明某個方法必須是重新定義父類別中的方法,編譯器得知這項資訊後,在編譯程式時如果發現被 @Override 標示的方法並非重新定義父類別中的方法,就會回報錯誤。

舉個例子來說,如果您在定義新類別時想要重新定義 Object 類別的 toString() 方法,您可能會寫成這樣:

  1. public class CustomClass {
  2. public String ToString() {
  3. return "customObject";
  4. }
  5. }

在撰寫 toString() 方法時,您因為打字錯誤或其它的疏忽,將之打成 ToString() 了,您編譯這個類別時並不會出現任何的錯誤,編譯器不會知道您是想重新定義 toString() 方法,只會當您是定義了一個新的 ToString() 方法。

您可以使用 java.lang.Override 這個 Annotation 型態,在方法上加上一個 @Override的Annotation,這可以告訴編譯器您現在定義的這個方法,必須是重新定義父類別中的同名方法。

範例 17.1 CustomClass.java

  1. package onlyfun.caterpillar;
  2. public class CustomClass {
  3. @Override
  4. public String ToString() {
  5. return "customObject";
  6. }
  7. }

在編譯程式時,編譯器看到 @Override 這個 Annotation,瞭解到必須檢查被標示的方法是不是重新定義了父類別的 ToString() 方法,但父類別中並沒有 ToString() 這個方法,所以編譯器會回報錯誤:

  1. CustomClass.java:4: method does not override a method from its superclass
  2. @Override
  3. ^
  4. 1 error

重新修改一下範例 17.1 中的 ToString() 為 toString(),編譯時就不會有問題了。

範例 17.2 CustomClass2.java

  1. package onlyfun.caterpillar;
  2. public class CustomClass2{
  3. @Override
  4. public String toString() {
  5. return "customObject";
  6. }
  7. }

java.lang.Override 是個 Marker annotation,簡單的說就是用於標示的 Annotation,Annotation 名稱本身即表示了要給工具程式的資訊,例如 Override 這個名稱告知編譯器,被 @Override 標示的方法必須是重新定義父類別中的同名方法。

良葛格的話匣子 「Annotation 型態」與「Annotation」實際上是有所區分的,Annotation 是 Annotation 型態的實例,例如 @Override 是個 Annotation,它是 java.lang.Override 型態的一個實例,一個文件中可以有很多個 @Override,但它們都是屬於 java.lang.Override 型態。

17.1.2 標示方法為 Deprecated @Deprecated

java.lang.Deprecated 是 J2SE 5.0 中標準的 Annotation 型態之一,它對編譯器說明某個方法已經不建議使用,如果有開發人員試圖使用或重新定義被 @Deprecated 標示的方法,編譯器必須提出警示訊息。

舉個例子來說,您可能定義一個 Something 類別,並在當中定義有 getSomething() 方法,而在這個類別被實際使用一段時間之後,您不建議開發人員使用 getSomething() 方法了,並想要將這個方法標示為 “deprecated”,您可以使用 @Deprecated 在 getSomething() 方法加上標示。

範例 17.3 Something.java

  1. package onlyfun.caterpillar;
  2. public class Something {
  3. @Deprecated public Something getSomething() {
  4. return new Something();
  5. }
  6. }

如果有人試圖在繼承這個類別後重新定義 getSomething() 方法,或是在程式中呼叫使用 getSomething() 方法,則編譯時會有警訊出現,例如範例 17.4。

範例 17.4 SomethingDemo.java

  1. package onlyfun.caterpillar;
  2. public class SomethingDemo {
  3. public static void main(String[] args) {
  4. Something some = new Something();
  5. // 呼叫被@Deprecated標示的方法
  6. some.getSomething();
  7. }
  8. }

編譯範例 17.4 時,就會出現以下的警訊:

  1. Note: SomethingDemo.java uses or overrides a deprecated API.
  2. Note: Recompile with -Xlint:deprecation for details.

想要知道詳細的警訊內容的話,可以在編譯時加上 -Xlint:deprecation 引數,編譯器會告訴您是因為您使用了某個被 @Deprecated 標示了的方法而提出警訊,加上 -Xlint:deprecation 引數顯示的完整訊息如下:

  1. javac -Xlint:deprecation -d . SomethingDemo.java
  2. SomethingDemo.java:6: warning: [deprecation] getSomething() in
  3. onlyfun.caterpillar.Something has been deprecated
  4. some.getSomething();
  5. ^
  6. 1 warning

java.lang.Deprecated 也是個 Marker annotation,簡單的說就是用於標示,Annotation 名稱本身即包括了要給工具程式的資訊,例如 Deprecated 這個名稱在告知編譯器,被 @Deprecated 標示的方法是一個不建議被使用的方法,如果有開發人員不小心使用了被 @Deprecated 標示的方法,編譯器要提出警訊提醒開發人員。

17.1.3 抑制編譯器警訊 @SuppressWarnings

java.lang.SuppressWarnings 是 J2SE 5.0 中標準的 Annotation 型態之一,它對編譯器說明某個方法中若有警示訊息,則加以抑制,不用在編譯完成後出現警訊,不過事實上這個功能在 Sun JDK 5.0 中沒有實現出來。

在這邊說明 @SuppressWarnings 的功能,考慮範例 17.5 的 SomeClass 類別。

範例 17.5 SomeClass.java

  1. package onlyfun.caterpillar;
  2. import java.util.*;
  3. public class SomeClass {
  4. public void doSomething() {
  5. Map map = new HashMap();
  6. map.put("some", "thing");
  7. }
  8. }

由於在 J2SE 5.0 中加入了集合物件的泛型功能,並建議您明確的指定集合物件中將內填的物件之型態,但在範例 17.5 的 SomeClass 類別中使用 Map 時並沒有指定內填物件之型態,所以在編譯時會出現以下的訊息:

  1. Note: SomeClass.java uses unchecked or unsafe operations.
  2. Note: Recompile with -Xlint:unchecked for details.

在編譯時一併指定 -Xlint:unchecked 可以看到警示的細節:

  1. javac -Xlint:unchecked -d . SomeClass.java
  2. SomeClass.java:8: warning: [unchecked] unchecked call to put(K,V)
  3. as a member of the raw type java.util.Map
  4. map.put("some", "thing");
  5. ^
  6. 1 warning

如果您想讓編譯器忽略這些細節,則可以使用 @SuppressWarnings 這個 Annotation。

範例 17.6 SomeClass2.java

  1. package onlyfun.caterpillar;
  2. import java.util.*;
  3. public class SomeClass2 {
  4. @SuppressWarnings(value={"unchecked"})
  5. public void doSomething() {
  6. Map map = new HashMap();
  7. map.put("some", "thing");
  8. }
  9. }

這麼一來,編譯器將忽略掉 “unchecked” 的警訊,您也可以指定忽略多個警訊:

  1. @SuppressWarnings(value={"unchecked", "deprecation"})

@SuppressWarnings 為所謂的 Single-value annotation,因為這樣的 Annotation 只有一個成員,稱為 value 成員,可在使用 Annotation 時作額外的資訊指定。

17.1.4 自訂 Annotation 型態

您可以自訂 Annotation 型態,並使用這些自訂的 Annotation 型態在程式碼中使用 Annotation,這些 Annotation 將提供資訊給您的程式碼分析工具。

首先來看看如何定義 Marker Annotation,也就是 Annotation 名稱本身即提供資訊,對於程式分析工具來說,主要是檢查是否有 Marker Annotation 的出現,並作出對應的動作。要定義一個 Annotation 所需的動作,就類似於定義一個介面(interface),只不過您使用的是 @interface,範例 17.7 定義一個 Debug Annotation 型態。

範例 17.7 Debug.java

  1. package onlyfun.caterpillar;
  2. public @interface Debug {}

由於是個 Marker Annotation,所以沒有任何的成員在 Annotation 定義當中,編譯完成後,您就可以在程式碼中使用這個 Annotation 了,例如:

  1. public class SomeObject {
  2. @Debug
  3. public void doSomething() {
  4. // ....
  5. }
  6. }

稍後可以看到如何在 Java 程式中取得 Annotation 資訊(因為要使用 Java 程式取得資訊,所以還要設定 meta-annotation,稍後會談到),接著來看看如何定義一個 Single-value annotation,它只有一個 value 成員,範例 17.8 是個簡單的示範。

範例 17.8 UnitTest.java

  1. package onlyfun.caterpillar;
  2. public @interface UnitTest {
  3. String value();
  4. }

實際上您定義了 value() 方法,編譯器在編譯時會自動幫您產生一個 value 的資料成員,接著在使用 UnitTest Annotation 時要指定值,例如:

  1. public class MathTool {
  2. @UnitTest("GCD")
  3. public static int gcdOf(int num1, int num2) {
  4. // ....
  5. }
  6. }

@UnitTest(“GCD”) 實際上是 @UnitTest(value=”GCD) 的簡便寫法,value 也可以是陣列值,例如定義一個 FunctionTest 的 Annotation 型態。

範例 17.9 FunctionTest.java

  1. package onlyfun.caterpillar;
  2. public @interface FunctionTest {
  3. String[] value();
  4. }

在使用範例 17.9 所定義的 Annotation 時,可以寫成 @FunctionTest({“method1”, “method2”}) 這樣的簡便形式,或是 @FunctionTest(value={“method1”, “method2”}) 這樣的詳細形式,您也可以對 value 成員設定預設值,使用 “default” 關鍵字即可。

範例 17.10 UnitTest2.java

  1. package onlyfun.caterpillar;
  2. public @interface UnitTest2 {
  3. String value() default "noMethod";
  4. }

這麼一來如果您使用 @UnitTest2 時沒有指定 value 值,則 value 預設就是 “noMethod”。
您也可以為 Annotation 定義額外的成員,以提供額外的資訊給分析工具,範例 17.11 定義使用列舉型態、String 與 boolean 型態來定義 Annotation 的成員。

範例 17.11 Process.java

  1. package onlyfun.caterpillar;
  2. public @interface Process {
  3. public enum Current {NONE, REQUIRE, ANALYSIS, DESIGN, SYSTEM};
  4. Current current() default Current.NONE;
  5. String tester();
  6. boolean ok();
  7. }

您可以如範例 17.12 使用範例 17.11 定義的 Annotation 型態。

範例 17.12 Application.java

  1. package onlyfun.caterpillar;
  2. public class Application {
  3. @Process(
  4. current = Process.Current.ANALYSIS,
  5. tester = "Justin Lin",
  6. ok = true
  7. )
  8. public void doSomething() {
  9. // ....
  10. }
  11. }

當您使用 @interface 自行定義 Annotation 型態時,實際上是自動繼承了 java.lang.annotation.Annotation 介面,並由編譯器自動為您完成其它產生的細節,並且在定義 Annotation 型態時,不能繼承其它的 Annotation 型態或是介面。

定義 Annotation 型態時也可以使用套件機制來管理類別,由於範例所設定的套件都是 onlyfun.caterpillar,所以您可以直接使用 Annotation 型態名稱而不指定套件名,但如果您是在別的套件下使用這些自訂的 Annotation,記得使用 import 告訴編譯器型態的套件位置,例如:

  1. import onlyfun.caterpillar.Debug;
  2. public class Test {
  3. @Debug
  4. public void doTest() {
  5. }
  6. }

或是使用完整的 Annotation 名稱,例如:

  1. public class Test {
  2. @onlyfun.caterpillar.Debug
  3. public void doTest() {
  4. }
  5. }

17.2 meta-annotation

所謂 meta-annotation 就是 Annotation 型態的資料,也就是 Annotation 型態的 Annotation,在定義 Annotation 型態的時候,為 Annotation 型態加上 Annotation 並不奇怪,這可以為處理 Annotation 型態的分析工具提供更多的資訊。

17.2.1 告知編譯器如何處理 annotaion @Retention

java.lang.annotation.Retention 型態可以在您定義 Annotation 型態時,指示編譯器該如何對待您的自定義的 Annotation 型態,預設上編譯器會將 Annotation 資訊留在 .class 檔案中,但不被虛擬機器讀取,而僅用於編譯器或工具程式運行時提供資訊。

在使用 Retention 型態時,需要提供 java.lang.annotation.RetentionPolicy 的列舉型態,RetentionPolicy 的定義如下所示:

  1. package java.lang.annotation;
  2. public enum RetentionPolicy {
  3. SOURCE, // 編譯器處理完Annotation資訊後就沒事了
  4. CLASS, // 編譯器將Annotation儲存於class檔中,預設
  5. RUNTIME // 編譯器將Annotation儲存於class檔中,可由VM讀入
  6. }

RetentionPolicy 為 SOURCE 的例子是 @SuppressWarnings,這個資訊的作用僅在編譯時期告知編譯器來抑制警訊,所以不必將這個資訊儲存於 .class 檔案。

RetentionPolicy 為 RUNTIME 的時機,可以像是您使用 Java 設計一個程式碼分析工具,您必須讓 VM 能讀出 Annotation 資訊,以便在分析程式時使用,搭配反射(Reflection)機制,就可以達到這個目的。

在 J2SE 5.0 新增了 java.lang.reflect.AnnotatedElement 這個介面,當中定義有四個方法:

  1. public Annotation getAnnotation(Class annotationType);
  2. public Annotation[] getAnnotations();
  3. public Annotation[] getDeclaredAnnotations();
  4. public boolean isAnnotationPresent(Class annotationType);

Class、Constructor、Field、Method、Package 等類別,都實作了 AnnotatedElement 介面,所以您可以從這些類別的實例上,分別取得標示於其上的 Annotation 與相關資訊,由於是在執行時期讀取 Annotation 資訊,所以定義 Annotation 時必須設定 RetentionPolicy 為 RUNTIME,也就是可以在 VM 中讀取 Annotation 資訊。

舉個例子來說,假設您設計了範例 17.13 的 Annotation。

範例 17.13 SomeAnnotation.java

  1. package onlyfun.caterpillar;
  2. import java.lang.annotation.Retention;
  3. import java.lang.annotation.RetentionPolicy;
  4. @Retention(RetentionPolicy.RUNTIME)
  5. public @interface SomeAnnotation {
  6. String value();
  7. String name();
  8. }

由於 RetentionPolicy 為 RUNTIME,編譯器在處理 SomeAnnotation 時,會將 Annotation 及給定的相關訊息編譯至 .class 檔中,並設定為 VM 可以讀出 Annotation 資訊,接著您可以如範例 17.14 來使用 SomeAnnotation。

範例 17.14 SomeClass3.java

  1. package onlyfun.caterpillar;
  2. public class SomeClass3 {
  3. @SomeAnnotation(
  4. value = "annotation value1",
  5. name = "annotation name1"
  6. )
  7. public void doSomething() {
  8. // ....
  9. }
  10. }

現在假設您要設計一個原始碼分析工具來分析您所設計的類別,一些分析時所需的資訊您已經使用 Annotation 標示於類別中了,您可以在執行時讀取這些 Annotation 的相關資訊,範例 17.15 是個簡單的示範。

範例 17.15 AnalysisApp.java

  1. package onlyfun.caterpillar;
  2. import java.lang.annotation.Annotation;
  3. import java.lang.reflect.Method;
  4. public class AnalysisApp {
  5. public static void main(String[] args) throws NoSuchMethodException {
  6. Class<SomeClass3> c = SomeClass3.class;
  7. // 因為SomeAnnotation標示於doSomething()方法上
  8. // 所以要取得doSomething()方法的Method實例
  9. Method method = c.getMethod("doSomething");
  10. // 如果SomeAnnotation存在的話
  11. if(method.isAnnotationPresent(SomeAnnotation.class)) {
  12. System.out.println("找到 @SomeAnnotation");
  13. // 取得SomeAnnotation
  14. SomeAnnotation annotation =
  15. method.getAnnotation(SomeAnnotation.class);
  16. // 取得value成員值
  17. System.out.println("\tvalue = " + annotation.value());
  18. // 取得name成員值
  19. System.out.println("\tname = " + annotation.name());
  20. }
  21. else {
  22. System.out.println("找不到 @SomeAnnotation");
  23. }
  24. // 取得doSomething()方法上所有的Annotation
  25. Annotation[] annotations = method.getAnnotations();
  26. // 顯示Annotation名稱
  27. for(Annotation annotation : annotations) {
  28. System.out.println("Annotation名稱:" +
  29. annotation.annotationType().getName());
  30. }
  31. }
  32. }

Annotation 標示於方法上的話,就要取得方法的 Method 代表實例,同樣的,如果 Annotation 標示於類別或套件上的話,就要分別取得類別的 Class 代表實例或是套件的 Package 代表實例,之後可以使用實例上的 getAnnotation() 等相關方法,以測試是否可取得 Annotation 或進行其它操作,範例 17.15 的執行結果如下所示:

  1. 找到 @SomeAnnotation
  2. value = annotation value1
  3. name = annotation name1
  4. Annotation名稱:onlyfun.caterpillar.SomeAnnotation

17.2.2 限定 annotation 使用對象 @Target

在定義 Annotation 型態時,您使用 java.lang.annotation.Target 可以定義其適用之時機,在定義時要指定 java.lang.annotation.ElementType 的列舉值之一:

  1. package java.lang.annotation;
  2. public enum ElementType {
  3. TYPE, // 適用 class, interface, enum
  4. FIELD, // 適用 field
  5. METHOD, // 適用 method
  6. PARAMETER, // 適用 method 上之 parametar
  7. CONSTRUCTOR, // 適用 constructor
  8. LOCAL_VARIABLE, // 適用區域變數
  9. ANNOTATION_TYPE, // 適用 annotation 型態
  10. PACKAGE // 適用 package
  11. }

舉個例子來說,假設您定義 Annotation 型態時,想要限定它只能適用於建構方法與方法成員,則您可以如範例 17.16 的方式來定義。

範例 17.16 MethodAnnotation.java

  1. package onlyfun.caterpillar;
  2. import java.lang.annotation.Target;
  3. import java.lang.annotation.ElementType;
  4. @Target({ElementType.CONSTRUCTOR, ElementType.METHOD})
  5. public @interface MethodAnnotation {}
  6. 如果您嘗試將MethodAnnotation標示於類別之上,例如:
  7. @onlyfun.caterpillar.MethodAnnotation
  8. public class SomeoneClass {
  9. public void doSomething() {
  10. // ....
  11. }
  12. }

則在編譯時會發生以下的錯誤:

  1. SomeObject.java:1: annotation type not applicable to this kind of declaration
  2. @onlyfun.caterpillar.MethodAnnotation
  3. ^
  4. 1 error

17.2.3 要求為 API 文件的一部份 @Documented

在製作 Java Doc 文件時,預設上並不會將 Annotation 的資料加入到文件中,例如您設計了以下的 OneAnnotation 型態:

  1. package onlyfun.caterpillar;
  2. public @interface OneAnnotation {}

然後將之用在以下的程式中:

  1. public class SomeoneClass {
  2. @onlyfun.caterpillar.OneAnnotation
  3. public void doSomething() {
  4. // ....
  5. }
  6. }

您可以試著使用 javadoc 程式來產生 Java Doc 文件,您會發現文件中並不會有 Annotation 的相關訊息。

預設 Annotation 不會記錄至 Java Doc 文件中

圖 17.1 預設 Annotation 不會記錄至 Java Doc 文件中

Annotation 用於標示程式碼以便分析工具使用相關資訊,有時 Annotation 包括了重要的訊息,您也許會想要在使用者製作 Java Doc 文件的同時,也一併將 Annotation 的訊息加入至 API 文件中,所以在定義 Annotation 型態時,您可以使用 java.lang.annotation.Documented,範例 17.17 是個簡單示範。

範例 17.17 TwoAnnotation.java

  1. package onlyfun.caterpillar;
  2. import java.lang.annotation.Documented;
  3. import java.lang.annotation.Retention;
  4. import java.lang.annotation.RetentionPolicy;
  5. @Documented
  6. @Retention(RetentionPolicy.RUNTIME)
  7. public @interface TwoAnnotation {}

使用 java.lang.annotation.Documented 為您定義的 Annotation 型態加上 Annotation 時,您必須同時使用 Retention 來指定編譯器將訊息加入 .class 檔案,並可以由 VM 讀取,也就是要設定 RetentionPolicy 為 RUNTIME,接著您可以使用這個 Annotation,並產生 Java Doc 文件,這次可以看到文件中包括了 @TwoAnnotation 的訊息。

Annotation 記錄至 Java Doc 文件中

圖 17.2 Annotation 記錄至 Java Doc 文件中

良葛格的話匣子 您可以使用搜尋引擎找到一堆有關如何製作 Java Doc 文件的說明,您也可以參考Sun網站上的文章:

17.2.4 子類是否繼承父類的 annotation @Inherited

在您定義 Annotation 型態並使用於程式碼上後,預設上父類別中的 Annotation 並不會被繼承至子類別中,您可以在定義 Annotation 型態時加上 java.lang.annotation.Inherited 型態的 Annotation,這讓您定義的 Annotation 型態在被繼承後仍可以保留至子類別中。

範例 17.18 ThreeAnnotation.java

  1. package onlyfun.caterpillar;
  2. import java.lang.annotation.Retention;
  3. import java.lang.annotation.RetentionPolicy;
  4. import java.lang.annotation.Inherited;
  5. @Retention(RetentionPolicy.RUNTIME)
  6. @Inherited
  7. public @interface ThreeAnnotation {
  8. String value();
  9. String name();
  10. }
  11. 您可以在下面這個程式中使用@ThreeAnnotation
  12. public class SomeoneClass {
  13. @onlyfun.caterpillar.ThreeAnnotation(
  14. value = "unit",
  15. name = "debug1"
  16. )
  17. public void doSomething() {
  18. // ....
  19. }
  20. }

如果您有一個類別繼承了 SomeoneClass 類別,則 @ThreeAnnotation 也會被繼承下來。

17.3 接下來的主題

每一個章節的內容由淺至深,初學者該掌握的深度要到哪呢?在這個章節中,對於初學者我建議至少掌握以下幾點內容:

下一個章節是個捨遺補缺的章節,也是本書的最後一個章節,當中說明了一些本書中有使用到但還沒有詳細說明的 API,另外我還介紹了簡單的訊息綁定,這讓您在配置程式的文字訊息時能夠更有彈性。