程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java中利用Reflection API優化代碼

Java中利用Reflection API優化代碼

編輯:關於JAVA

摘要

開發者通過各種各樣的方法來嘗試避免單調冗余的編程。一些編程的規則例如繼承、多態或者設計模型可以幫助開發者避免產生多余的代碼。不過由於軟件開發方面存在著不確定性,因此這些規則並不能消除代碼維護和重新編寫的需要。在很多時候維護都是不可避免的,只有不能運作的軟件才是從不需要維護的。不過,這篇文章介紹了你可以使用Java的Reflection API的功能來減少單調的代碼編寫,並可以使用活動的代碼產生來克服reflection的限制。

數據配置(由外部的源頭得到數據並且將它裝載到一個Java對象中)可以利用reflection的好處來創建一個可重用的方案。問題是很簡單的:將數據由一個文件裝入到一個對象的字段中。現在假設用作數據的目標Java類每星期改變一次?有一個很直接的解決方法,不過你必須不斷地維護載入的過程來反映任何的改變。在更復雜的環境下,同樣的問題可能會令系統崩潰掉。對於一個處理過運用XML的大型系統的人來說,他就會遇到過這個問題。要編寫一個載入的過程通常是非常單調乏味的,由於數據源或者目標Java類的改變,你需要經常更新和重新編寫代碼。在這裡我要介紹另一個解決方案,那就是使用映射,它通常使用更少的編碼,並且可以在目標Java類發生改變後更新自己。

最初,我想介紹一個使用Reflection在運行期間配置數據的方案。在開始的時候,一個動態、基於映射的程序要比一個簡單的方法更有吸引力多了。隨後,我要揭示出運行時Reflection的復雜性和冒險性。這篇文章將介紹由運行時的Reflection到活動的代碼產生。

由簡單到復雜

我的第一個方案使用一個載入類將數據從一個文件載入到對象中。我的源代碼含有對StringTokenizer對象下一節點方法的多次調用。在修改多次後,我的編碼邏輯變得非常的直接、系統化。該類構造了專用的代碼。在這個初始方案中,我只需要使用3個基本的對象:

1、Strings

2、Objects

3、Arrays of objects

你可以影射類的對象來產生代碼塊,如下表所示:

被影射來產生代碼塊的對象

Field type Code block String fileIterator.nextString(); Object[] Vector collector = new Vector(); while(fileIterator.hasMoreDataForArray()){ Object data = initializeObject(fileIterator)collector.add(data); } Object[] objArray = new Object[collector.size()]; collector.copyInto(objArray); Object initializeObject(fileIterator);

**************表一**************

我已經使用這個方案作了幾次編碼,因此我在寫代碼之前我已經知道該方案和代碼的結構。難點在於該類是變化的。類的名字、成份和結構在任何時候都可能發生變化,而任何的改變你都要重新編寫代碼。雖然會發生這些變化,但是結構和下載的流程仍然是一樣的;在寫代碼前,我仍然知道代碼的結構和成份。我需要一個方法,來將頭腦中的編碼流程轉換為一個可重用的、自動的形式。由於我是一個有效率的編程者,我很快就厭倦了編寫幾乎一樣的代碼,這時我想到了映射。

數據配置通常需要一個源到目的數據的影射。影射可以是一個圖解、DTD(document type definition,文檔類型定義),文件格式等。在這個例子中,映射將一個對象的類定義解釋為我們要映射的流程。映射可以在運行時復制代碼的功能。在需要重寫代碼時,我將載入的過程用映射來代替,它所需要的時間和重寫是一樣的。

載入的工程可以概括為以下幾步:

1、解釋:一個影射決定你在構造一個對象時需要些什麼

2、請求數據:要滿足構造的需要,要進行一個調用來得到數據

3、拖:數據由源中得到。

4、推:數據被填充入一個對象的新實例

5、如果必要的話,重復步驟1

你需要以下的類來滿足以上的步驟:

.數據類(Data classes):由ASCII文件中的數據實例化。類定義提供數據的影射。數據類必須滿足以下的條件:

.它們必須包含有一個構造器來接收全部必需的參數,以使用一個有效的狀態來構造對象;

.它們必須由對象構成,這些對象是reflective過程知道如何處理的

.對象裝載器(Object loader):使用reflection和數據類作為一個影射來載入數據。產生數據請求。

.載入管理器(Load manager):作為對象裝載器和數據源的中介層,將對數據的請求轉換為一個數據指定的調用。這可以令對象載入器做到與數據源無關。通過它的接口和一個可載入的類對象通信。

.數據循環接口(Data iterator interface):載入管理器和載入類對象使用這個接口來由數據源中得到數據。

一旦你創建了支持的類,你就可以使用以下的聲明來創建和影射一個對象:

FooFileIterator iter = new FooFileIterator(fileLocation, log);
LoadManager manager = new FooFileLoadManager(iter);
SubFooObject obj =
(SubFooObject)ReflectiveObjectLoader.initializeInstance(SubFooObject.class, manager,log);

通過這個處理,你就創建了一個包含有文件內容的SubFooObject實例。

局限

開發者必須決定使用哪個方案來解決問題是最好的;通常做出這個決定是最困難的部分。在考慮使用reflection作數據配置時,你要考慮到以下一些限制:

1、不要令一個簡單的問題復雜化。reflection是比較復雜的,因此在必要的時候才使用它。一旦開發者明白了reflection的能力,他就想使用它來解決所有的問題。如果你有更快、更簡單的方案來解決問題時,你就不應該使用reflection(即使這個更好的方案可能使用更多的代碼)。reflection是強大的,但也有一些風險。

2、考慮性能。reflection對性能的影響比較大,因為要在運行時發現和管理類屬性需要時間和內存。

重新評估方案

如上所述,使用運行時reflection的第一個限制是“不要令簡單的問題復雜化”。在使用reflection時,這是不可避免的。將reflection和遞歸結合起來是一個令人頭痛的問題;重新看代碼也是一件可怕的事情;而准確決定代碼的功能也是非常復雜的。要知道代碼的准確作用的唯一方法是使用一些取樣數據,逐行地看,就象運行時一樣。不過,對於每個可能的數據組合都使用這種方式幾乎是不可能的。在這種情況下,使用單元測試代碼可能有些幫助,不過也很可能出現錯誤。幸運的是,還有一個可選的方法。

可選的方法

由上面列出的限制可以看到,在某些情況下,使用reflective載入過程可能是得不償失的。代碼產生提供了一個通用的選擇方法。你也可以使用reflection來檢查一個類並且為載入過程產生代碼。

Andrew Hunt和David Thomas介紹了兩類的代碼產生器,見The Pragmatic Programmer(http://www.javaworld.com/javaworld/jw-11-2001/jw-1102-codegen-p2.html#resources)

1、Passive(被動):被動的代碼產生器在實現代碼時需要人工的干預。許多的IDE(集成開發環境)都提供相應的向導來實現。

2、Active(主動):主動的代碼產生指的是代碼一旦創建,就不再需要修改了。如果有問題產生,這個問題也應該在代碼產生器中解決,而不是在產生的源文件中解決。在理想的情況下,這個過程應該包含在編譯的處理過程中,從而確保類不會過期。

代碼產生的優點和缺點包含有以下方面:

優點:

.簡單:產生的代碼通常是更便於開發者閱讀和調試。

.編譯過程的錯誤:Reflexive在運行時出現錯誤的機會要比編譯的期間多。例如,改變被載入的對象將有可能令產生的載入類拋出一個編譯的錯誤,不過reflexive過程將不會看到任何的區別,直到在運行時遇到這個類。

缺點:

.維護:使用被動的代碼產生,修改被載入的對象將需要更新或者重新產生載入的類。如果該類被重新產生,那麼自定義的東西就會丟失。

回頭再來看看主動代碼產生的好處

在這裡我們可以看到在運行時使用reflection是不可以接受的。主動的代碼產生有著reflection的全部好處,但是沒有它的限制。還可以繼續使用reflection,不過只是在代碼的產生過程,而不是運行的過程。理由如下:

1、更少冒險:運行時的reflection明顯是更冒險的,特別是問題變得復雜的時候。

2、基於單元測試,但並不是編譯器

3、多功能性:產生的代碼有著runtime reflection的全部好處,而且有著runtime reflection沒有的好處。

4、更易懂:雖然經過多次的處理,但是將遞歸和reflection結合仍然是很復雜的。產生源代碼的方式更加容易解釋和理解。代碼產生過程需要遞歸和reflection,但得到的結果是可查看的源代碼,而不是難以理解的東西。

寫代碼產生器

要寫一個代碼產生器,在思考的時候,你不能只是簡單地編寫一個方案來解決一個問題,你應該看得更遠。代碼產生器(以及reflection)需要你作更多的思考。如果你只是使用runtime reflection,你就不得不在運行時概念化問題,而不是使用簡單、兼容性好的源代碼來解決問題。代碼產生要求你從兩個方面來查看問題。代碼產生過程會將抽象的概念轉變為實際的源代碼。Runtime reflection則一直是抽象的。

代碼產生過程將你的思考過程轉變為代碼,然後產生並且編譯代碼。編譯器會讓你知道你的思考過程在語法上是否正確;單元測試則可以驗證代碼在運行時的行為。就動態特性方面,runtime reflection就不能達到這個級別的安全性。

代碼產生器

在經歷後幾次失敗的設計後,我認為最簡單的方法是:在載入過程中,為每一種需要實例化的類產生一個方法。一個方法工廠產生每個特別類的正確方法。一個代碼編譯對象緩沖來自代碼工廠的方法請求,以產生最終源代碼文件的內容。

MethodCode對象是代碼產生過程的核心。以下就是一個int的代碼產生對象的例子:

public class MethodForInt extends MethodCode {
private final static MethodParameter param = new MethodParameter(SimpleFileIterator.class, "parser");
public MethodForInt(Class type, CodeBuilder builder){
super(type, builder);
}
public MethodParameter[] getInputParameters(){
return new MethodParameter[]{
param
};
}
public MethodParameter[] getInstanceParameters(){
return getInputParameters();
}
protected String getImplBody(CodeBuilder builder){
return "return " + param.getName() + ".nextInt();
";
}
}

基類MethodCode完成全部的工作。在代碼產生的過程中,MethodCode類決定方法名字以及用作實現的框架代碼。MethodForInt類只需要為它的方法定義所有的數據規范。其中最重要的部分是getImplBody(CodeBuilder builder) 方法。這就是定義函數的地方。getInputParameters()和 getInstanceParameters()這兩個方法定義函數的簽名。函數簽名不但聲明了函數,而且還定義了如何在其它函數中調用它。MethodForInt類在代碼產生時產生以下的代碼:

/** Generated Load method for int**/
final public static int loadint(com.thoughtworks.rettig.util.SimpleFileIterator parser){
return parser.nextInt();
}

無縫產生

在編譯階段,代碼產生為源代碼產生帶來了額外的負擔。你可以使用一個方便的配置編譯工具(例如Ant)來處理這個問題。在這裡,我要為這篇文章的例子產生代碼,我創建了以下的任務:

dir = "."
fork = "yes">

兩個參數指定了源代碼的目的包,以及用來創建載入過程的類。一旦定義好任務並且將它集成到編譯的過程中,代碼產生就會成為編譯過程的一部分。

對比工作方案

對於這兩個工作方案,我們現在來回顧分析一下。

當你在運行時遇到問題時,這些方案的真正不同之處是顯而易見的。在runtime reflection的方案中,由於廣泛地使用reflection和遞歸,你可能得到的是一個難懂的堆棧跟蹤。產生代碼的方式可讓你得到一個簡單的堆棧跟蹤,這樣你就可以回溯到產生的源代碼作調試。

以下就是一個例子,由同樣的錯誤產生的兩種堆棧跟蹤。我將讓你判斷一下使用哪一種作調試。(要注意的是為了便於閱讀,我已經移除了com.thoughtworks.rettig包的限定)

Runtime Reflection Exception:
java.lang.NumberFormatException: itemName
at java.lang.Integer.parseInt(Integer.java:409)
at java.lang.Integer.parseInt(Integer.java:458)
at ...util.SimpleFileIterator.nextInt(SimpleFileIterator.java:82)
at ...dataLoader.SimpleFileLoadManager$1.load(SimpleFileLoadManager.java:44)
at ...dataLoader.ReflectiveObjectLoader.initializeInstance(ReflectiveObjectLoader.java:129)
at ...dataLoader.ReflectiveObjectLoader.constructObject(ReflectiveObjectLoader.java, Compiled Code)
at ...dataLoader.ReflectiveObjectLoader.initializeInstance(ReflectiveObjectLoader.java:134)
at ...dataLoader.ReflectiveObjectLoader.constructObjectArray(ReflectiveObjectLoader.java, Compiled Code)
at ...dataLoader.ReflectiveObjectLoader.initializeArray(ReflectiveObjectLoader.java:39)
at ...dataLoader.ReflectiveObjectLoader.initializeInstance(ReflectiveObjectLoader.java:123)
at ...dataLoader.ReflectiveObjectLoader.constructObject(ReflectiveObjectLoader.java, Compiled Code)
at ...dataLoader.ReflectiveObjectLoader.initializeInstance(ReflectiveObjectLoader.java:134)
at ...dataLoader.ReflectiveObjectLoader.initializeInstance(ReflectiveObjectLoader.java:103)

以下是產生代碼的Exception

java.lang.NumberFormatException: itemName
at java.lang.Integer.parseInt(Integer.java:409)
at java.lang.Integer.parseInt(Integer.java:458)
at ...util.SimpleFileIterator.nextInt(SimpleFileIterator.java:82)
at ....example.generated.PurchaseOrderLoader.loadint(PurchaseOrderLoader.java:32)
at ....example.generated.PurchaseOrderLoader.loadLineItem(PurchaseOrderLoader.java:22)
at ....example.generated.PurchaseOrderLoader.loadLineItemArray(PurchaseOrderLoader.java, Compiled Code)
at ....example.generated.PurchaseOrderLoader.loadPurchaseOrder(PurchaseOrderLoader.java:27)

對於runtime reflection,我們要分離出問題的話需要作很多的記錄日志。在載入的過程中,大量地使用記錄日志明顯是不適合的。使用reflection,你可以令堆棧跟蹤更加有意義,不過這會令已經復雜的環境更加復雜化。使用產生代碼的方法時,得到的代碼只是記下runtime reflection將如何處理這些情形。

這兩種實現方式在性能方面也有著區別。我驚奇地發現,在使用runtime reflection時,我的例子載入要慢4到7倍。

一個典型的運行結果如下所示:

java com.thoughtworks.rettig.example.TestPerformance
Number of Iterations: 100000
Generated
Total time: 14481
Max Memory Used: 1337672
Reflection
Total time: 89219
Max Memory Used: 1407944

這個延遲可以歸結於在運行時,reflection需要時間來發現類的屬性,而產生代碼的方法只是由顯式的調用構成。Runtime reflection使用的內存也要多一些,但並不是多很多。當然,reflection可以作更好的優化,但是該優化將會非常復雜,而且優化的結果可能也遠遠比不上一個直接的方案。

相反地,產生代碼方式的優化是一件輕而易舉的事情。在以前的一項目中使用了類似的代碼產生器,我通過優化載入的過程從而使用更少的內存。我只需要幾分鐘變可以將代碼產生器修改好。在優化後代碼產生了一個bug,不過堆棧跟蹤很直接地指出了代碼產生過程中的問題,我很快就改正過來了。在runtime reflection時,我將不會嘗試作同樣的優化,因為實在是太費勁了。

運行源代碼

如果你查看一下源代碼,你將可以更好地掌握這裡談到的幾個問題。要編譯和運行源代碼,這需要將其中的文件解壓到一個空的目錄,然後在命令行運行ant Install。這樣將會使用Ant的編譯腳本來產生、編譯源代碼,並且作單元測試。(這裡假定你已經安裝了Ant和JUnit 3.7)

我創建了一個例子,它是一個簡單的購買訂單,該訂單由幾類對象組成。JUnit測試案例解釋了你如何使用每個方法從一個文件創造一個購買訂單。測試案例然後驗證對象的內容,以確保數據被正確地裝載。你可以由以下的包中得到測試的內容和所有支持的類:

com.thoughtworks.rettig.example
com.thoughtworks.rettig.example.reflection
com.thoughtworks.rettig.example.generated

在兩個測試案例之間最值得注意的不同是runtime reflection無需支持代碼來裝載數據。這就是reflection的神奇所在。它僅需要類定義和源數據的位置來載入數據,而產生代碼的方式在它可以創建測試案例前,需要一個產生的載入類。

在對象創建過程中,兩者是非常相似的。以下就是reflection的代碼:

SimpleFileIterator iter = new SimpleFileIterator(fileLocation);
LoadManager manager = new SimpleFileLoadManager(iter);
PurchaseOrder obj = (PurchaseOrder) ReflectiveObjectLoader.initializeInstance(PurchaseOrder.class, manager);

以下就是產生的代碼:

SimpleFileIterator iter = new SimpleFileIterator(file);
PurchaseOrder po = PurchaseOrderLoader.loadPurchaseOrder(iter);

總結

reflection的好處是非常明顯的。當與代碼產生結合時,它就成為一個無價的、更重要的、安全的工具。通常沒有其它的方式來進行許多表面上多余的任務。對於代碼產生:我用得越多,就越喜歡它。通過不斷地修改和改進功能,代碼變得更為清晰易懂,而runtime reflection的效果則相反,我加入的功能越多,它就變得越復雜。

所以,如果你感到將來要使用reflection來解決一個復雜的問題,要記得以下一條規律:不要在runtime時做。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved