- 第 17 章 Annotation
- 17.1 Annotation
- 17.2 meta-annotation
- 17.3 接下來的主題
第 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() 方法,您可能會寫成這樣:
public class CustomClass {
public String ToString() {
return "customObject";
}
}
在撰寫 toString() 方法時,您因為打字錯誤或其它的疏忽,將之打成 ToString() 了,您編譯這個類別時並不會出現任何的錯誤,編譯器不會知道您是想重新定義 toString() 方法,只會當您是定義了一個新的 ToString() 方法。
您可以使用 java.lang.Override 這個 Annotation 型態,在方法上加上一個 @Override的Annotation,這可以告訴編譯器您現在定義的這個方法,必須是重新定義父類別中的同名方法。
範例 17.1 CustomClass.java
package onlyfun.caterpillar;
public class CustomClass {
@Override
public String ToString() {
return "customObject";
}
}
在編譯程式時,編譯器看到 @Override 這個 Annotation,瞭解到必須檢查被標示的方法是不是重新定義了父類別的 ToString() 方法,但父類別中並沒有 ToString() 這個方法,所以編譯器會回報錯誤:
CustomClass.java:4: method does not override a method from its superclass
@Override
^
1 error
重新修改一下範例 17.1 中的 ToString() 為 toString(),編譯時就不會有問題了。
範例 17.2 CustomClass2.java
package onlyfun.caterpillar;
public class CustomClass2{
@Override
public String toString() {
return "customObject";
}
}
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
package onlyfun.caterpillar;
public class Something {
@Deprecated public Something getSomething() {
return new Something();
}
}
如果有人試圖在繼承這個類別後重新定義 getSomething() 方法,或是在程式中呼叫使用 getSomething() 方法,則編譯時會有警訊出現,例如範例 17.4。
範例 17.4 SomethingDemo.java
package onlyfun.caterpillar;
public class SomethingDemo {
public static void main(String[] args) {
Something some = new Something();
// 呼叫被@Deprecated標示的方法
some.getSomething();
}
}
編譯範例 17.4 時,就會出現以下的警訊:
Note: SomethingDemo.java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.
想要知道詳細的警訊內容的話,可以在編譯時加上 -Xlint:deprecation 引數,編譯器會告訴您是因為您使用了某個被 @Deprecated 標示了的方法而提出警訊,加上 -Xlint:deprecation 引數顯示的完整訊息如下:
javac -Xlint:deprecation -d . SomethingDemo.java
SomethingDemo.java:6: warning: [deprecation] getSomething() in
onlyfun.caterpillar.Something has been deprecated
some.getSomething();
^
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
package onlyfun.caterpillar;
import java.util.*;
public class SomeClass {
public void doSomething() {
Map map = new HashMap();
map.put("some", "thing");
}
}
由於在 J2SE 5.0 中加入了集合物件的泛型功能,並建議您明確的指定集合物件中將內填的物件之型態,但在範例 17.5 的 SomeClass 類別中使用 Map 時並沒有指定內填物件之型態,所以在編譯時會出現以下的訊息:
Note: SomeClass.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
在編譯時一併指定 -Xlint:unchecked 可以看到警示的細節:
javac -Xlint:unchecked -d . SomeClass.java
SomeClass.java:8: warning: [unchecked] unchecked call to put(K,V)
as a member of the raw type java.util.Map
map.put("some", "thing");
^
1 warning
如果您想讓編譯器忽略這些細節,則可以使用 @SuppressWarnings 這個 Annotation。
範例 17.6 SomeClass2.java
package onlyfun.caterpillar;
import java.util.*;
public class SomeClass2 {
@SuppressWarnings(value={"unchecked"})
public void doSomething() {
Map map = new HashMap();
map.put("some", "thing");
}
}
這麼一來,編譯器將忽略掉 “unchecked” 的警訊,您也可以指定忽略多個警訊:
@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
package onlyfun.caterpillar;
public @interface Debug {}
由於是個 Marker Annotation,所以沒有任何的成員在 Annotation 定義當中,編譯完成後,您就可以在程式碼中使用這個 Annotation 了,例如:
public class SomeObject {
@Debug
public void doSomething() {
// ....
}
}
稍後可以看到如何在 Java 程式中取得 Annotation 資訊(因為要使用 Java 程式取得資訊,所以還要設定 meta-annotation,稍後會談到),接著來看看如何定義一個 Single-value annotation,它只有一個 value 成員,範例 17.8 是個簡單的示範。
範例 17.8 UnitTest.java
package onlyfun.caterpillar;
public @interface UnitTest {
String value();
}
實際上您定義了 value() 方法,編譯器在編譯時會自動幫您產生一個 value 的資料成員,接著在使用 UnitTest Annotation 時要指定值,例如:
public class MathTool {
@UnitTest("GCD")
public static int gcdOf(int num1, int num2) {
// ....
}
}
@UnitTest(“GCD”) 實際上是 @UnitTest(value=”GCD) 的簡便寫法,value 也可以是陣列值,例如定義一個 FunctionTest 的 Annotation 型態。
範例 17.9 FunctionTest.java
package onlyfun.caterpillar;
public @interface FunctionTest {
String[] value();
}
在使用範例 17.9 所定義的 Annotation 時,可以寫成 @FunctionTest({“method1”, “method2”}) 這樣的簡便形式,或是 @FunctionTest(value={“method1”, “method2”}) 這樣的詳細形式,您也可以對 value 成員設定預設值,使用 “default” 關鍵字即可。
範例 17.10 UnitTest2.java
package onlyfun.caterpillar;
public @interface UnitTest2 {
String value() default "noMethod";
}
這麼一來如果您使用 @UnitTest2 時沒有指定 value 值,則 value 預設就是 “noMethod”。
您也可以為 Annotation 定義額外的成員,以提供額外的資訊給分析工具,範例 17.11 定義使用列舉型態、String 與 boolean 型態來定義 Annotation 的成員。
範例 17.11 Process.java
package onlyfun.caterpillar;
public @interface Process {
public enum Current {NONE, REQUIRE, ANALYSIS, DESIGN, SYSTEM};
Current current() default Current.NONE;
String tester();
boolean ok();
}
您可以如範例 17.12 使用範例 17.11 定義的 Annotation 型態。
範例 17.12 Application.java
package onlyfun.caterpillar;
public class Application {
@Process(
current = Process.Current.ANALYSIS,
tester = "Justin Lin",
ok = true
)
public void doSomething() {
// ....
}
}
當您使用 @interface 自行定義 Annotation 型態時,實際上是自動繼承了 java.lang.annotation.Annotation 介面,並由編譯器自動為您完成其它產生的細節,並且在定義 Annotation 型態時,不能繼承其它的 Annotation 型態或是介面。
定義 Annotation 型態時也可以使用套件機制來管理類別,由於範例所設定的套件都是 onlyfun.caterpillar,所以您可以直接使用 Annotation 型態名稱而不指定套件名,但如果您是在別的套件下使用這些自訂的 Annotation,記得使用 import 告訴編譯器型態的套件位置,例如:
import onlyfun.caterpillar.Debug;
public class Test {
@Debug
public void doTest() {
}
}
或是使用完整的 Annotation 名稱,例如:
public class Test {
@onlyfun.caterpillar.Debug
public void doTest() {
}
}
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 的定義如下所示:
package java.lang.annotation;
public enum RetentionPolicy {
SOURCE, // 編譯器處理完Annotation資訊後就沒事了
CLASS, // 編譯器將Annotation儲存於class檔中,預設
RUNTIME // 編譯器將Annotation儲存於class檔中,可由VM讀入
}
RetentionPolicy 為 SOURCE 的例子是 @SuppressWarnings,這個資訊的作用僅在編譯時期告知編譯器來抑制警訊,所以不必將這個資訊儲存於 .class 檔案。
RetentionPolicy 為 RUNTIME 的時機,可以像是您使用 Java 設計一個程式碼分析工具,您必須讓 VM 能讀出 Annotation 資訊,以便在分析程式時使用,搭配反射(Reflection)機制,就可以達到這個目的。
在 J2SE 5.0 新增了 java.lang.reflect.AnnotatedElement 這個介面,當中定義有四個方法:
public Annotation getAnnotation(Class annotationType);
public Annotation[] getAnnotations();
public Annotation[] getDeclaredAnnotations();
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
package onlyfun.caterpillar;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface SomeAnnotation {
String value();
String name();
}
由於 RetentionPolicy 為 RUNTIME,編譯器在處理 SomeAnnotation 時,會將 Annotation 及給定的相關訊息編譯至 .class 檔中,並設定為 VM 可以讀出 Annotation 資訊,接著您可以如範例 17.14 來使用 SomeAnnotation。
範例 17.14 SomeClass3.java
package onlyfun.caterpillar;
public class SomeClass3 {
@SomeAnnotation(
value = "annotation value1",
name = "annotation name1"
)
public void doSomething() {
// ....
}
}
現在假設您要設計一個原始碼分析工具來分析您所設計的類別,一些分析時所需的資訊您已經使用 Annotation 標示於類別中了,您可以在執行時讀取這些 Annotation 的相關資訊,範例 17.15 是個簡單的示範。
範例 17.15 AnalysisApp.java
package onlyfun.caterpillar;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
public class AnalysisApp {
public static void main(String[] args) throws NoSuchMethodException {
Class<SomeClass3> c = SomeClass3.class;
// 因為SomeAnnotation標示於doSomething()方法上
// 所以要取得doSomething()方法的Method實例
Method method = c.getMethod("doSomething");
// 如果SomeAnnotation存在的話
if(method.isAnnotationPresent(SomeAnnotation.class)) {
System.out.println("找到 @SomeAnnotation");
// 取得SomeAnnotation
SomeAnnotation annotation =
method.getAnnotation(SomeAnnotation.class);
// 取得value成員值
System.out.println("\tvalue = " + annotation.value());
// 取得name成員值
System.out.println("\tname = " + annotation.name());
}
else {
System.out.println("找不到 @SomeAnnotation");
}
// 取得doSomething()方法上所有的Annotation
Annotation[] annotations = method.getAnnotations();
// 顯示Annotation名稱
for(Annotation annotation : annotations) {
System.out.println("Annotation名稱:" +
annotation.annotationType().getName());
}
}
}
Annotation 標示於方法上的話,就要取得方法的 Method 代表實例,同樣的,如果 Annotation 標示於類別或套件上的話,就要分別取得類別的 Class 代表實例或是套件的 Package 代表實例,之後可以使用實例上的 getAnnotation() 等相關方法,以測試是否可取得 Annotation 或進行其它操作,範例 17.15 的執行結果如下所示:
找到 @SomeAnnotation
value = annotation value1
name = annotation name1
Annotation名稱:onlyfun.caterpillar.SomeAnnotation
17.2.2 限定 annotation 使用對象 @Target
在定義 Annotation 型態時,您使用 java.lang.annotation.Target 可以定義其適用之時機,在定義時要指定 java.lang.annotation.ElementType 的列舉值之一:
package java.lang.annotation;
public enum ElementType {
TYPE, // 適用 class, interface, enum
FIELD, // 適用 field
METHOD, // 適用 method
PARAMETER, // 適用 method 上之 parametar
CONSTRUCTOR, // 適用 constructor
LOCAL_VARIABLE, // 適用區域變數
ANNOTATION_TYPE, // 適用 annotation 型態
PACKAGE // 適用 package
}
舉個例子來說,假設您定義 Annotation 型態時,想要限定它只能適用於建構方法與方法成員,則您可以如範例 17.16 的方式來定義。
範例 17.16 MethodAnnotation.java
package onlyfun.caterpillar;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD})
public @interface MethodAnnotation {}
如果您嘗試將MethodAnnotation標示於類別之上,例如:
@onlyfun.caterpillar.MethodAnnotation
public class SomeoneClass {
public void doSomething() {
// ....
}
}
則在編譯時會發生以下的錯誤:
SomeObject.java:1: annotation type not applicable to this kind of declaration
@onlyfun.caterpillar.MethodAnnotation
^
1 error
17.2.3 要求為 API 文件的一部份 @Documented
在製作 Java Doc 文件時,預設上並不會將 Annotation 的資料加入到文件中,例如您設計了以下的 OneAnnotation 型態:
package onlyfun.caterpillar;
public @interface OneAnnotation {}
然後將之用在以下的程式中:
public class SomeoneClass {
@onlyfun.caterpillar.OneAnnotation
public void doSomething() {
// ....
}
}
您可以試著使用 javadoc 程式來產生 Java Doc 文件,您會發現文件中並不會有 Annotation 的相關訊息。
圖 17.1 預設 Annotation 不會記錄至 Java Doc 文件中
Annotation 用於標示程式碼以便分析工具使用相關資訊,有時 Annotation 包括了重要的訊息,您也許會想要在使用者製作 Java Doc 文件的同時,也一併將 Annotation 的訊息加入至 API 文件中,所以在定義 Annotation 型態時,您可以使用 java.lang.annotation.Documented,範例 17.17 是個簡單示範。
範例 17.17 TwoAnnotation.java
package onlyfun.caterpillar;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface TwoAnnotation {}
使用 java.lang.annotation.Documented 為您定義的 Annotation 型態加上 Annotation 時,您必須同時使用 Retention 來指定編譯器將訊息加入 .class 檔案,並可以由 VM 讀取,也就是要設定 RetentionPolicy 為 RUNTIME,接著您可以使用這個 Annotation,並產生 Java Doc 文件,這次可以看到文件中包括了 @TwoAnnotation 的訊息。
圖 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
package onlyfun.caterpillar;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Inherited;
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface ThreeAnnotation {
String value();
String name();
}
您可以在下面這個程式中使用@ThreeAnnotation:
public class SomeoneClass {
@onlyfun.caterpillar.ThreeAnnotation(
value = "unit",
name = "debug1"
)
public void doSomething() {
// ....
}
}
如果您有一個類別繼承了 SomeoneClass 類別,則 @ThreeAnnotation 也會被繼承下來。
17.3 接下來的主題
每一個章節的內容由淺至深,初學者該掌握的深度要到哪呢?在這個章節中,對於初學者我建議至少掌握以下幾點內容:
- 會使用 @Override、@Deprecated、@SuppressWarnings
- 知道如何在 Java 程式中讀出 Annotation 資訊
下一個章節是個捨遺補缺的章節,也是本書的最後一個章節,當中說明了一些本書中有使用到但還沒有詳細說明的 API,另外我還介紹了簡單的訊息綁定,這讓您在配置程式的文字訊息時能夠更有彈性。