Eclipse3.1中體驗J2SE5.0之注釋類型
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一個最簡單的注釋
除了定義清單1中的注釋以外,我們還可以在注釋(Annotation)中加入域定義。方法很簡單,不需定義Getter和Setter方法,而只需一個簡單的方法,比如:
清單2 為注釋加入域
定義了這個注釋之后,我們在程序中引用就可以使用這個注釋了。
清單3 使用自定義的注釋
由于TODO中只定義了一個域,使用TODO的時候,可以簡寫為
清單4 單域注釋的簡寫
類似的,你可以在你的注釋(Annotation)類型中定義多個域,也可以為每個域定義缺省值。比如:
清單5定義缺省值
如果定義了缺省值,在使用的時候可以不用再賦值。比如:
清單6使用定義了缺省值的注釋
在這個例子中,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) 注釋的類型
從上面的例子中,我們可以看出,按照使用者所需要傳入的參數數目, 注釋(Annotation)的類型可以分為三種。
第一種是標記注釋類型:
標記注釋(Marker)是最簡單的注釋, 不需要定義任何域。下面要介紹的Override和Deprecated都是標記類型的。當然,如果一個注釋類型提供了所有域的缺省值,那么這個注釋類型也可以認為是一個注釋類型。使用標記類型的語法很簡單。
清單7 標記注釋的用法
第二種是單值注釋類型:單值注釋類型只有一個域。語法也很簡單:
清單8 單值注釋的用法
第三種是全值注釋類型。 全注釋類型其實并不算是一個真正的類型,只是使用注釋類型完整的語法:
清單9 全值注釋的用法
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,這個擔心就是不必要的。如果你拼錯了你希望重寫的方法,編譯器會報錯,告訴你父類沒有相應的方法。
清單10給出了模板方法的一個例子。這個例子中有定義了兩個類,SubClass和BaseClass。其中SubClass繼承了BaseClass,并且希望重寫BaseClass的方法doPartII()。然而SubClass中錯誤的拼寫了這個方法的名稱。圖4顯示了SubClass中的編譯錯誤。熟悉eclipse的讀者會看到在編輯器里出現了Error Marker,說明這一行有編譯錯誤。將鼠標指向這行,顯示了錯誤信息。
清單10 模板方法
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的用法
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注釋的定義
清單13 TOFORMATE的定義
清單14 使用注釋的類MyCalculator
清單15動態查找注釋
下面我們來運行這個例子,這個例子的運行結果如圖10所示。
運行結果和我們先前的定義是一致的。在運行時,我們可以獲得注釋使用的相關信息。
在我們介紹了什么是注釋以后,你可能會想知道注釋可以應用到什么地方呢?使用注釋有什么好處呢?在下面的小節中我們將介紹一個稍復雜的例子。從這個例子中,你將體會到注釋所以提供的強大的描述機制(declarative programming)。
2) 使用注釋替代Visitor模式
在J2SE 5.0以前,我們在設計應用的時候,我們經常會使用Visitor這個設計模式。Visitor這個模式一般是用于為我們已經設計好了一組類添加方法,而不需要擔心改變定義好的類。比如說我們已經定義了好了一組類結構,但是我們希望將這些類的對象部分數據輸出到某種格式的文件中。
Vistor模式的實現 使用Vistor模式,首先我們在Employee這個類中加入export方法,export方法如圖7所示。Export方法接受Exporter對象作為參數,并在方法體中調用exporter對象的visit()方法。
在這里我們定義了一個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的定義
清單17-清單20中給出了包含數據的這些類的定義以及這些類是如何使用注釋Exportable的。 圖18定義了Main函數,使用ExporterGenerator來產生輸出文件。清單21給出了使用注釋來實現這一功能的兩個類:ExporterGenerator和TXTExporterGenerator。其中ExporterGenerator定義了一個基本的框架。而TXTExporterGenerator繼承了ExporterGenerator,并且重寫了outputField方法,在這個方法中實現了特定格式的輸出。用戶可以繼承這個ExporterGenerator,并且實現其中的抽象方法來定義自己期望的格式。 清單17 Employee的類定義
清單18 Regular的類定義
清單19 Vendor的類定義
清單20 Contractor的類定義
清單21 Supplemental的類定義
清單22使用ExportableGenerator的程序
清單23 ExportableGenerator
清單24 TXTExportableGenerator
在這個例子中,我們將一個Employee對象的部分內容輸出到文件C:est.output中。圖8顯示了這個例子的輸出結果。
通過這種方法,我們可以動態生成Employee對象的域輸出,而不需要在程序中寫明要輸出哪些確定的域。如果需要更為豐富的格式,我們可以定義多個注釋類型。通過對不同注釋以及屬性的解析,實現格式化的文件輸出。
4、注釋類型的小結
所謂元數據,指的是關于信息的信息。一般而言,代碼分析工具,測試工具或者部署工具會使用元數據來產生配置信息以及使用配置信息產生控制邏輯。這些工具通常使用Java的反射特性,重構元數據的信息,并對這些信息進行解釋。
新的技術會不斷改變程序設計和開發人員的設計思想。那么注釋(Annotation)給我們帶來了什么呢? 僅僅在代碼分析,或者是開發測試框架和部署框架的時候才有用么? 我認為并不是這樣。從上面的例子可以看出,注釋(Annotation)的應用范圍其實是很廣泛的。在我們的應用中充分的利用元數據,可以提高的軟件的質量和可維護性。
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:est.output中。圖8顯示了這個例子的輸出結果。
![]() 圖8 輸出結果 |
通過這種方法,我們可以動態生成Employee對象的域輸出,而不需要在程序中寫明要輸出哪些確定的域。如果需要更為豐富的格式,我們可以定義多個注釋類型。通過對不同注釋以及屬性的解析,實現格式化的文件輸出。
4、注釋類型的小結
所謂元數據,指的是關于信息的信息。一般而言,代碼分析工具,測試工具或者部署工具會使用元數據來產生配置信息以及使用配置信息產生控制邏輯。這些工具通常使用Java的反射特性,重構元數據的信息,并對這些信息進行解釋。
新的技術會不斷改變程序設計和開發人員的設計思想。那么注釋(Annotation)給我們帶來了什么呢? 僅僅在代碼分析,或者是開發測試框架和部署框架的時候才有用么? 我認為并不是這樣。從上面的例子可以看出,注釋(Annotation)的應用范圍其實是很廣泛的。在我們的應用中充分的利用元數據,可以提高的軟件的質量和可維護性。







