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開發環境中開發枚舉類型, 註釋類型和範型應用。本文將介紹註釋類型。
註釋類型
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、定義註釋
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 註釋嚮導生成的代碼 |
1) 註釋的類型
從上面的例子中,我們可以看出,按照使用者所需要傳入的參數數目, 註釋(Annotation)的類型可以分爲三種。
第一種是標記註釋類型:
標記註釋(Marker)是最簡單的註釋, 不需要定義任何域。下面要介紹的Override和Deprecated都是標記類型的。當然,如果一個註釋類型提供了所有域的缺省值,那麼這個註釋類型也可以認爲是一個註釋類型。使用標記類型的語法很簡單。
清單7 標記註釋的用法
@MarkerAnnotation |
第二種是單值註釋類型:單值註釋類型只有一個域。語法也很簡單:
清單8 單值註釋的用法
@SingleValueAnnotation("some value") |
第三種是全值註釋類型。 全註釋類型其實並不算是一個真正的類型,只是使用註釋類型完整的語法:
清單9 全值註釋的用法
@MultipleValueAnnotation( key1=value1, key2=value2, key3=value3, ) |
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,圖3)非常有用。熟悉設計模式的讀者們一定知道,模板方法中通常定義了抽象類,並且這個抽象類中定義了主要的控制流。子類就是通過重寫父類中控制流中所調用的方法來實現自己的邏輯。有的時候,父類會將這些方法定義爲抽象方法,但是有的時候也會提供缺省實現。在後者的情況下,子類可以不實現這個方法。
這樣就帶來一個問題,如果你希望在子類中重寫這個方法,但是無意中寫錯了方法的名字,這個錯誤是很難被發現的。因爲你希望重寫的這個方法,會被編譯器當作一個新的方法而不是重寫父類的方法。而現在使用@Override,這個擔心就是不必要的。如果你拼錯了你希望重寫的方法,編譯器會報錯,告訴你父類沒有相應的方法。
圖3 模板方法的類圖 |
清單10給出了模板方法的一個例子。這個例子中有定義了兩個類,SubClass和BaseClass。其中SubClass繼承了BaseClass,並且希望重寫BaseClass的方法doPartII()。然而SubClass中錯誤的拼寫了這個方法的名稱。圖4顯示了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"); } } |
圖4 Override應用的例子 |
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)所修飾。
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) 使用註釋替代Visitor模式
在J2SE 5.0以前,我們在設計應用的時候,我們經常會使用Visitor這個設計模式。Visitor這個模式一般是用於爲我們已經設計好了一組類添加方法,而不需要擔心改變定義好的類。比如說我們已經定義了好了一組類結構,但是我們希望將這些類的對象部分數據輸出到某種格式的文件中。
Vistor模式的實現 使用Vistor模式,首先我們在Employee這個類中加入export方法,export方法如圖7所示。Export方法接受Exporter對象作爲參數,並在方法體中調用exporter對象的visit()方法。
圖7 使用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中。圖8顯示了這個例子的輸出結果。
圖8 輸出結果 |
通過這種方法,我們可以動態生成Employee對象的域輸出,而不需要在程序中寫明要輸出哪些確定的域。如果需要更爲豐富的格式,我們可以定義多個註釋類型。通過對不同註釋以及屬性的解析,實現格式化的文件輸出。
4、註釋類型的小結
所謂元數據,指的是關於信息的信息。一般而言,代碼分析工具,測試工具或者部署工具會使用元數據來產生配置信息以及使用配置信息產生控制邏輯。這些工具通常使用Java的反射特性,重構元數據的信息,並對這些信息進行解釋。
新的技術會不斷改變程序設計和開發人員的設計思想。那麼註釋(Annotation)給我們帶來了什麼呢? 僅僅在代碼分析,或者是開發測試框架和部署框架的時候纔有用麼? 我認爲並不是這樣。從上面的例子可以看出,註釋(Annotation)的應用範圍其實是很廣泛的。在我們的應用中充分的利用元數據,可以提高的軟件的質量和可維護性。