J2SE 5.0 (Tiger)的發布是Java語言發展史上的一個重要的裡程碑, 是迄今為止在 Java 編程方面所取得的最大進步。
J2SE 5.0提供了很多令人激動的特性。這些特性包括范型(generics)的支持, 枚舉類 型(enumeration)的支持, 元數據(metadata)的支持, 自動拆箱(unboxing)/裝箱 (autoboxing), 可變個數參數(varargs), 靜態導入(static imports), 以及新的線程架 構(Thread framework)。
隨著J2SE 5.0的推出, 越來越多的集成開發環境(IDE)支持J2SE 5.0的開發。著名的開 源Java IDE Eclipse從3.1M4開始支持J2SE 5.0的開發, 目前最新的版本是3.1RC4。
本系列將介紹J2SE 5.0中三個比較重要的特性: 枚舉類型, 注釋類型, 范型, 並在此 基礎上介紹在如何在Eclipse 3.1開發環境中開發枚舉類型, 注釋類型和范型應用。本文 將介紹注釋類型。
2. 注釋類型
2.1注釋類型簡介
J2SE 5.0提供了很多新的特性。其中的一個很重要的特性,就是對元數據(Metadata) 的支持。在J2SE5.0中,這種元數據叫作注釋(Annotation)。通過使用注釋, 程序開發人 員可以在不改變原有邏輯的情況下,在源文件嵌入一些補充的信息。代碼分析工具,開發 工具和部署工具可以通過這些補充信息進行驗證或者進行部署。舉個例子,比如說你希望 某個方法的參數或者返回值不為空,雖然我們可以在Java doc中說明,但是表達同樣意思 的說法有很多,比如"The return value should not be null"或者"null is not allowed here"。測試工具很難根據這些語言來分析出程序員所期望的前提條件(Pre- condition)和執行後的條件(Post-condition)。而使用注釋(Annotation),這個問題就可 以輕而易舉的解決了。
2.2定義注釋
J2SE5.0支持用戶自己定義注釋。定義注釋很簡單,注釋是由@Interface關鍵字來聲明 的。比如下面是一個最簡單的注釋(Annotation)。
清單1一個最簡單的注釋
public @interface TODO{}
除了定義清單1中的注釋以外,我們還可以在注釋(Annotation)中加入域定義。方法 很簡單,不需定義Getter和Setter方法,而只需一個簡單的方法,比如:
清單2 為注釋加入域
public @interface TODO{
String priority();
}
定義了這個注釋之後,我們在程序中引用就可以使用這個注釋了。
清單3 使用自定義的注釋
@TODO(
priority="high"
)
public void calculate(){
//body omission
}
由於TODO中只定義了一個域,使用TODO的時候,可以簡寫為
清單4 單域注釋的簡寫
@TODO("high")
類似的,你可以在你的注釋(Annotation)類型中定義多個域,也可以為每個域定義 缺省值。比如:
清單5定義缺省值
public @interface TODO{
String priority();
String owner();
boolean testable() default true;
}
如果定義了缺省值,在使用的時候可以不用再賦值。比如:
清單6使用定義了缺省值的注釋
@TODO(
priority="high",
owner="Catherine"
)
public void calculate(){
//body omission
}
在這個例子中,testable用缺省值true。
和上文一樣,我們使用Eclipse 3.1作為集成的編譯運行環境。Eclipse 3.1提供了向 導幫助用戶來定義注釋。1.首先我們創建一個Plug-in 項目, com.catherine.lab.annotation.demo。在Package Explorer中選中包package com.catherine.lab.annotation.demo, 2.點擊New->Other->Java->Annotation ,彈出了下面的對話框。4.輸入注釋的名稱,在這裡例子中輸入TODO, 點擊Finish, 圖2 中的注釋就生成了。
圖1 創建注釋向導
圖2 注釋向導生成的代碼
2.2.1注釋的類型
從上面的例子中,我們可以看出,按照使用者所需要傳入的參數數目, 注釋 (Annotation)的類型可以分為三種。
第一種是標記注釋類型:
標記注釋(Marker)是最簡單的注釋, 不需要定義任何域。下面要介紹的Override和 Deprecated都是標記類型的。當然,如果一個注釋類型提供了所有域的缺省值,那麼這個 注釋類型也可以認為是一個注釋類型。使用標記類型的語法很簡單。
清單7 標記注釋的用法
@MarkerAnnotation
第二種是單值注釋類型:單值注釋類型只有一個域。語法也很簡單:
清單8 單值注釋的用法
@SingleValueAnnotation("some value")
第三種是全值注釋類型。全注釋類型其實並不算是一個真正的類型,只是使用注釋類 型完整的語法:
清單9 全值注釋的用法
@MultipleValueAnnotation(
key1=value1,
key2=value2,
key3=value3,
)
2.2.2 J2SE的內建注釋(build-in annotation)
在程序中不僅可以使用自己定義的注釋,還可以使用J2SE5.0中內建的注釋類型。下面 我們就詳細來介紹J2SE5.0提供的注釋類型。J2SE 5.0中預定義了三種注釋注釋類型:
Override :java.lang.Override 表示當前的方法重寫了父類的某個方法,如果父類 的對應的方法並不存在,將會發生編譯錯誤。
Deprecated:java.lang.Deprecated 表示 並不鼓勵使用當前的方法或者域變量。
SuppressWarnings: java.lang.SuppressWarnings關閉編譯器告警,這樣,在編譯1.5 之前的代碼的時候,不會出現大量不關心的無關的告警。
下面舉一個使用Override的例子。Override這個注釋類型在使用模板方法(Template Method,圖2)非常有用。熟悉設計模式的讀者們一定知道,模板方法中通常定義了抽象 類,並且這個抽象類中定義了主要的控制流。子類就是通過重寫父類中控制流中所調用的 方法來實現自己的邏輯。有的時候,父類會將這些方法定義為抽象方法,但是有的時候也 會提供缺省實現。在後者的情況下,子類可以不實現這個方法。
這樣就帶來一個問題,如果你希望在子類中重寫這個方法,但是無意中寫錯了方法的 名字,這個錯誤是很難被發現的。因為你希望重寫的這個方法,會被編譯器當作一個新的 方法而不是重寫父類的方法。而現在使用@Override,這個擔心就是不必要的。如果你拼 錯了你希望重寫的方法,編譯器會報錯,告訴你父類沒有相應的方法。
圖2 模板方法的類圖
清單10給出了模板方法的一個例子。這個例子中有定義了兩個類,SubClass和 BaseClass。其中SubClass繼承了BaseClass,並且希望重寫BaseClass的方法doPartII( )。然而SubClass中錯誤的拼寫了這個方法的名稱。圖3顯示了SubClass中的編譯錯誤。 熟悉eclipse的讀者會看到在編輯器裡出現了Error Marker,說明這一行有編譯錯誤。將 鼠標指向這行,顯示了錯誤信息。
清單10 模板方法
public abstract class BaseClass{ //模板方法的基類
public void doWork(){
doPartI(); //先調用doPartI()方法
doPartII();//之後調用doPartII()方法
}
public abstract void doPartI();
public void doPartII(){
}
}
public class SubClass extend BaseClass{
public void doPartI(){
};
@Override
public void doPortII(){//拼寫錯誤,產生編譯錯誤
System.out.println("override the method of superclass");
}
}
圖3 Override應用的例子
2.2.3 注釋的注釋
值得注意的是,J2SE5.0還提供了四種用於注釋的注釋類型。有以下的四種:
1. Target:用來指定這個注釋(Annotation)是為哪種類型而定義的。比如,這個類 型可能只是為method定義的。比如override,不能用@override來修飾class或者field。
比如清單11中定義了一個注釋:TODO,而這個注釋定義了Target為 ElementType.method。因此,TODO只能用來修飾方法,不能用來修飾類或者類變量。圖5 中給出了一個非法使用TODO的例子。在MyCalculator中,定義了一個布爾型的變量 isReady,如果用TODO來修飾這個類變量的話,會出現編譯錯誤。而用TODO來修飾方法 calculateRate(),則不會出現編譯錯誤。這是因為TODO的定義已經規定了,只能用來修 飾方法。
清單11 Target的用法
@Target({ElementType.METHOD})
public @interface TODO {
int priority() default 0;
}
圖5 TODO注釋的非法使用
2.Retention:Retention的策略可以從以下三種中選取:
RetentionPolicy.SOURCE:編譯器編譯之後會會從class file中除去注釋 (Annotation)。
Retention.CLASS:注釋(Annotation)保留在class file中,但是VM不會處理。
RetentionPolicy.RUNTIME,:注釋(Annotation)保留在class file,VM會進行處理。
請注意,如果你希望在運行時查找到這些注釋在什麼地方被用到,一定要在定義注釋 的時候,選擇RetentionPolicy.RUNTIME,否則即使你用注釋修飾了類變量或者方法,在運 行時也沒有辦法獲得這個信息的。
3.Documented:這個注釋(Annotation)將作為public API的一部分。
4.Inherited : 假設注釋(Annotation)定義的時候使用了Inherited,那麼如果這個 注釋(Annotation)修飾某個class,這個類的子類也被這個注釋(Annotation)所修飾 。
2.3注釋的應用
下面各小節顯示了在哪些情況下可以使用注釋以及如何使用注釋。
2.3.1動態查找注釋
當我們定義好了注釋以後,我們可以開發一些分析工具來解釋這些注釋。這裡通常要 用到Java的反射特性。比如說我們希望找到某個對象/方法/域使用了哪些注釋,或者獲得 某個特定的注釋,或者判斷是否使用某個特定的注釋, 我們可以參考下面這個例子。這個 例子中定義了兩個注釋:TODO和TOFORMATE。在MyCalculator類中,TODO用來修飾方法 calculateRate,而TOFORMATE用來修飾類變量concurrency和debitDate。而在類 TestCalculator的main函數中,通過Java反射特性,我們查找到使用這些注釋的類變量和 方法。清單12-清單15分別顯示這些類的定義。
清單12 TODO注釋的定義
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TODO {
int priority() default 0;
}
清單13 TOFORMATE的定義
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TOFORMATE {
}
清單14 使用注釋的類MyCalculator
public class MyCalculator {
boolean isReady;
@TOFORMATE double concurrency;
@TOFORMATE Date debitDate;
public MyCalculator() {
super();
}
@TODO
public void calculateRate(){
System.out.println("Calculating...");
}
}
清單15動態查找注釋
public class TestCalculator {
public static void main(String[] args) {
MyCalculator cal = new MyCalculator();
cal.calculateRate();
try {
Class c = cal.getClass();
Method[] methods = c.getDeclaredMethods();
for (Method m: methods) {
// 判斷這個方法有沒有使用TODO
if (m.isAnnotationPresent(TODO.class))
System.out.println("Method "+m.getName()+": the TODO is present");
}
Field[] fields = c.getDeclaredFields();
for (Field f : fields) {
// 判斷這個域有沒有使用TOFORMATE
if (f.isAnnotationPresent(TOFORMATE.class))
System.out.println("Field "+f.getName()+": the TOFORMATE is present");
}
} catch (Exception exc) {
exc.printStackTrace();
}
}
}
下面我們來運行這個例子,這個例子的運行結果如圖10所示。
運行結果和我們先前的定義是一致的。在運行時,我們可以獲得注釋使用的相關信息 。
圖6 運行結果
在我們介紹了什麼是注釋以後,你可能會想知道注釋可以應用到什麼地方呢?使用注 釋有什麼好處呢?在下面的小節中我們將介紹一個稍復雜的例子。從這個例子中,你將體 會到注釋所以提供的強大的描述機制(declarative programming)。
2.3.2 使用注釋替代Visitor模式
在J2SE 5.0以前,我們在設計應用的時候,我們經常會使用Visitor這個設計模式。 Visitor這個模式一般是用於為我們已經設計好了一組類添加方法,而不需要擔心改變定 義好的類。比如說我們已經定義了好了一組類結構,但是我們希望將這些類的對象部分數 據輸出到某種格式的文件中。
Vistor模式的實現
使用Vistor模式,首先我們在Employee這個類中加入export方法,export方法如圖11 所示。Export方法接受Exporter對象作為參數,並在方法體中調用exporter對象的visit ()方法。
圖11 使用Vistor模式實現格式輸出
在這裡我們定義了一個Exporter抽象類,我們可以通過繼承Exporter類,重寫其visit 方法來實現不同格式的文件輸出。圖11種給出visit方法的實現是一個簡單的例子。如果 要實現輸出成XML格式的,可以定義Exporter子類:XMLExporter。如果希望輸出成文本的 可以定義TXTExporter。但是這樣做不夠靈活的地方在於,如果Employee加入其他的域變 量,那麼相應的visitor類也需要進行修改。這就違反了面向對象Open for Extension, close for Modification的原則。
使用注釋替代Vistor模式
使用注釋(Annotation),也可以完成數據輸出的功能。首先定義一個新的注釋類型 :@Exportable。然後定義一個抽象的解釋器ExportableGenerator,將Employee 對象傳 入解釋器。在解釋器中,查找哪些域使用了Exportable這個注釋(Annotation),將這些 域(Field)按照一定格式輸出。圖12給出了Exportable注釋的定義。
清單16注釋Exportable的定義
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Exportable {
}
清單17-清單20中給出了包含數據的這些類的定義以及這些類是如何使用注釋 Exportable的。圖18定義了Main函數,使用ExporterGenerator來產生輸出文件。清單21 給出了使用注釋來實現這一功能的兩個類:ExporterGenerator和TXTExporterGenerator 。其中ExporterGenerator定義了一個基本的框架。而TXTExporterGenerator繼承了 ExporterGenerator,並且重寫了outputField方法,在這個方法中實現了特定格式的輸出 。用戶可以繼承這個ExporterGenerator,並且實現其中的抽象方法來定義自己期望的格 式。
清單17 Employee的類定義
public abstract class Employee {
public abstract String getName();
public abstract String getEmpNo();
public Employee() {
super();
}
}
清單18 Regular的類定義
public class Regular extends Employee{
@Exportable String name;
@Exportable String address;
@Exportable String title;
@Exportable String phone;
@Exportable String location;
@Exportable Date onboardDate;
@Exportable ArrayList<Employee> team;
String empNo;
public Regular(String name, String address, String title, String phone,
String location, Date date) {
super();
this.name = name;
this.address = address;
this.title = title;
this.phone = phone;
this.location = location;
onboardDate = date;
team = new ArrayList<Employee>();
}
public void addMemeber(Employee e){
team.add(e);
}
@Override
public String getName() {
// TODO Auto-generated method stub
return name;
}
}
清單19 Vendor的類定義
public class Vendor extends Employee {
@Exportable String name;
@Exportable String company;
@Exportable String team;
@Exportable String workingHours;
String empNo;
public Vendor(String name, String company, String team, String hours) {
super();
this.name = name;
this.company = company;
this.team = team;
workingHours = hours;
}
}
清單20 Contractor的類定義
public class Contractor extends Employee{
@Exportable String name;
@Exportable String company;
@Exportable String contractDuration;
String empNo;
public Contractor(String name, String company) {
super();
// TODO Auto-generated constructor stub
this.name = name;
this.company = company;
contractDuration ="1";
}
}
清單21 Supplemental的類定義
public class Contractor extends Employee{
@Exportable String name;
@Exportable String company;
@Exportable String contractDuration;
String empNo;
public Contractor(String name, String company) {
super();
this.name = name;
this.company = company;
contractDuration ="1";
}
}
清單22使用ExportableGenerator的程序
public class TestExportable {
public TestExportable() {
super();
}
public static void main(String[] args) {
Regular em=new Regular("Catherine","IBM","Software Engineer","82888288","BJ", new Date());
Employee vn1=new Vendor("Steve","IBM","PVC","8");
Employee vn2=new Vendor("Steve","IBM","PVC","8");
Employee ct=new Contractor("Joe","IBM");
Employee sup=new Supplemental("Linda","IBM","8");
em.addMemeber(vn1);
em.addMemeber(vn2);
em.addMemeber(ct);
em.addMemeber(sup);
PrintWriter ps;
try {
ps = new PrintWriter(new FileOutputStream(new File("C:\\test.output"),true));
ExportableGenerator eg=new TXTExportableGenerator(ps);
eg.genDoc(em,0);
eg.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
清單23 ExportableGenerator
public abstract class ExportableGenerator {
PrintWriter out = null;
public ExportableGenerator(PrintWriter out) {
super();
this.out = out;
}
public void genDoc(Employee e, int tagNum) {
Class employee = e.getClass();
Field[] fields = employee.getDeclaredFields();
outputFieldHeader(out,e);
for (Field f : fields) {
if (f.isAnnotationPresent(Exportable.class)) {
if (f.getType() != ArrayList.class) {
for(int i=0; i<tagNum;i++){
out.print("***");
}
outputSimpleField(out, f, e);
}else{
try {
ArrayList team=(ArrayList)f.get(e);
out.println("-----------------------------");
for(int i=0;i <team.size();i++){
Employee member=(Employee)team.get(i);
genDoc(member,tagNum+1);
out.println("-----------------------------");
}
} catch (IllegalArgumentException e1) {
e1.printStackTrace();
} catch (IllegalAccessException e1) {
e1.printStackTrace();
}
}
}
}
outputFieldFooter(out,e);
}
public void flush(){
out.flush();
out.close();
}
protected String value(Field f, Object obj) {
Class type = f.getType();
try {
if (type == String.class)
return (String) f.get(obj);
if (type == Date.class) {
return DateFormat.getDateInstance().format((Date)f.get(obj));
}
} catch (IllegalArgumentException e) {
e.printStackTrace();
return f.getName();
} catch (IllegalAccessException e) {
e.printStackTrace();
return f.getName();
}
return f.getName();
}
protected abstract void outputSimpleField(PrintWriter out, Field f,
Object obj);
protected abstract void outputFieldHeader(PrintWriter out,Object e);
protected abstract void outputFieldFooter(PrintWriter out,Object e);
清單24 TXTExportableGenerator
public class TXTExportableGenerator extends ExportableGenerator {
public TXTExportableGenerator(PrintWriter out) {
super(out);
}
@Override
protected void outputSimpleField(PrintWriter out, Field f,Object obj) {
out.print(f.getName());
out.print("=");
out.print(value(f,obj));
out.print(";");
out.println();
}
@Override
protected void outputFieldHeader(PrintWriter out,Object e) {
}
@Override
protected void outputFieldFooter(PrintWriter out,Object e) {
//out.println(e.getClass().getName()+":");
}
}
在這個例子中,我們將一個Employee對象的部分內容輸出到文件C:\test.output中。圖 19顯示了這個例子的輸出結果。
圖12 輸出結果
通過這種方法,我們可以動態生成Employee對象的域輸出,而不需要在程序中寫明要 輸出哪些確定的域。如果需要更為豐富的格式,我們可以定義多個注釋類型。通過對不同 注釋以及屬性的解析,實現格式化的文件輸出。
2.4注釋類型的小結
所謂元數據,指的是關於信息的信息。一般而言,代碼分析工具,測試工具或者部署 工具會使用元數據來產生配置信息以及使用配置信息產生控制邏輯。這些工具通常使用 Java的反射特性,重構元數據的信息,並對這些信息進行解釋。
新的技術會不斷改變程序設計和開發人員的設計思想。那麼注釋(Annotation)給我 們帶來了什麼呢? 僅僅在代碼分析,或者是開發測試框架和部署框架的時候才有用麼? 我認為並不是這樣。從上面的例子可以看出,注釋(Annotation)的應用范圍其實是很廣 泛的。在我們的應用中充分的利用元數據,可以提高的軟件的質量和可維護性。