Java 编程的动态性,第3部分: 应用反射--转载
在?上個月的文章中,我介紹了Java Reflection API,并簡要地講述了它的一些基本功能。我還仔細研究了反射的性能,并且在文章的最后給出了一些指導方針,告訴讀者在一個應用程序中何時應該使用反射,何時不應該使用反射。在本月這一期的文章中,我將通過查看一個應用程序來更深入地討論這一問題,這個應用程序是用于命令行參數(shù)處理的一個庫,它能夠很好地體現(xiàn)反射的強項和弱點。
一開始,在真正進入編寫實現(xiàn)代碼的工作之前,我將首先定義要解決的問題,然后為這個庫設(shè)計一個接口。不過,在開發(fā)這個庫的時候,我并不是按照上述步驟進行的――我先是盡力簡化一群有公共代碼基礎(chǔ)的應用程序中的現(xiàn)有代碼,然后使之通用化。本文中使用的“定義-設(shè)計-構(gòu)建”這種線性序列比起完完整整地描述開發(fā)過程要簡練得多,而且,按照這種方式來組織對開發(fā)過程的描述,我可以修正我原先的一些假設(shè),并清理掉這個庫的代碼中一些不必要的方面。您完全有希望發(fā)現(xiàn)將上述方式作為開發(fā)您自己的基于反射的應用程序時所使用的模型十分管用。
定義問題
我曾經(jīng)寫過許多使用命令行參數(shù)的Java應用程序。一開始,大多數(shù)應用程序都很小,但最后有些應用程序卻變得大到出乎我的意料。下面是我觀察到的這些應用程序的變大過程的標準模式:
當我進入到第5步的時候,我通常會后悔沒有將整個過程都放在第一步來做。好在我很快就會忘記后面的那些階段,不到一兩個星期,我又會考慮另外一個簡單的小命令行程序,我想擁有這個應用程序。有了這個想法之后,上述整個惡心的循環(huán)過程的重現(xiàn)只是時間的問題。
有一些庫可以用來幫助進行命令行參數(shù)處理。不過,在本文中我會忽略掉這些庫,而是自己動手創(chuàng)建一個庫。這不是(或者不僅僅是)因為我有著“非此處發(fā)明(not invented here)”的態(tài)度(即不愿意用外人發(fā)明的東西,譯者注),而是因為想拿參數(shù)處理作為一個實例。這樣一來,反射的強項和弱點便正好體現(xiàn)了對參數(shù)處理庫的需求。特別地,參數(shù)處理庫:
- 需要一個靈活的接口,用以支持各種應用程序。
- 對于每個應用程序,都必須易于配置。
- 不要求頂級的性能,因為參數(shù)只需處理一次。
- 不存在訪問安全性問題,因為命令行應用程序運行的時候通常不帶安全管理器。
這個庫中實際的反射代碼只代表整個實現(xiàn)的一小部分,因此我將主要關(guān)注與反射最相關(guān)的一些方面。如果您想找到有關(guān)這個庫的更多內(nèi)容(或許還想將它用到您自己的簡單命令行應用程序中去),您可以在?參考資料部分找到指向Web站點的鏈接。
草擬出一份設(shè)計
應用程序訪問參數(shù)數(shù)據(jù)最方便的方式或許是通過該應用程序的 main 對象的一些字段。例如,假設(shè)您正在編寫一個用于生成業(yè)務計劃的應用程序。您可能想使用一個?boolean?標記來控制業(yè)務計劃是簡要的還是冗長的,使用一個?int?作為第一年的收入,使用一個?String?作為對產(chǎn)品的描述。我將把這些會影響應用程序的運行的變量稱作?形參(parameters),以便與命令行提供的?實參(arguments)――即形參的值區(qū)分開來。通過為這些形參使用字段,將使得在 需要形參的應用程序代碼中的任何地方都可以方便地調(diào)用它們。而且,如果使用字段的話,在定義形參字段時為任意形參設(shè)置默認值也很方便,如清單1所示:
清單 1.業(yè)務計劃生成器(部分清單)
public class PlanGen {private boolean m_isConcise; // rarely used, default falseprivate int m_initialRevenue = 1000; // thousands, default is 1Mprivate float m_growthRate = 1.5; // default is 50% growth rateprivate String m_productDescription = // McD look out, here I come"eFood - (Really) Fast Food Online";...private int revenueForYear(int year) {return (int)(m_initialRevenue * Math.pow(m_growthRate, year-1));}...反射將使得應用程序可以直接訪問這些私有字段,允許參數(shù)處理庫在應用程序代碼中沒有任何特殊鉤子的情況下設(shè)置參數(shù)的值。但是我?的確需要某種方法能讓這個庫將這些字段與特定的命令行參數(shù)相關(guān)起來。在我能夠定義一個參數(shù)和一個字段之間的這種關(guān)聯(lián)如何與庫進行通信之前,我需要決定我希望如何格式化這些命令行參數(shù)。
對于本文,我將定義一種命令行格式,這是UNIX慣例的一種簡化版本。形參的實參值可以以任何順序提供,在最前面使用一個連字符以指示一個實參給出了一個或者多個單字符的形參標記(與實際的形參的值相 對)。對于這個業(yè)務計劃生成器,我將采用以下形參標記字符:
- c -- 簡要計劃
- f -- 第一年收入(千美元)
- g -- 增長率(每年)
- n -- 產(chǎn)品名稱
boolean?形參只需標記字符本身就可以設(shè)置一個值,而其他類型的形參還需要某種附加的實參信息。對于數(shù)值實參,我只將它的值緊跟在形參標記字符之后 (這意味著數(shù)字不能用作標記字符),而對于帶?String?類型值的形參,我將在命令行中使用跟在標記字符后面的實參作為實際的值。最后,如果還需要一些形參(例如業(yè)務計劃生成器的輸出文件的文件名),我假設(shè)這些形參的實參值跟在命令行中可選形參值的后面。有了上面給出的這些約定,業(yè)務計劃生成器的命令行看上去就是這個樣子:
java PlanGen -c -f2500 -g2.5 -n "iSue4U - Litigation at Internet Speed" plan.txt
如果把它放在一起,那么每個實參的意思就是:
- -c?-- 生成簡要計劃
- -f2500?-- 第一年收入為 $2,500,000
- -g2.5?-- 每年增長率為250%
- -n "iSue4U . . ."?-- 產(chǎn)品名稱是 "iSue4U . . ."
- plan.txt?-- 需要的輸出文件名
這時,我已經(jīng)得到了參數(shù)處理庫的基本功能的規(guī)范說明書。下一步就是為這個應用代碼定義一個特定的接口,以使用這個庫。
回頁首
選擇接口
您可以使用單個的調(diào)用來負責命令行參數(shù)的實際處理,但是這個應用程序首先需要以某種方式將它的特定的形參定義到庫中。這些形參可以具有不同的幾種類型(對于業(yè)務計劃生成器的例子,形參的類型可以是?boolean?,?int、float?和?java.lang.String?)。每種類型可能又有一些特殊的需求。例如,如果給出了標記字符的話,將?boolean?形參定義為?false?會比較好,而不是總將它定義為?true?。而且,為一個?int?值定義一個有效范圍也很有用。
我處理這些不同需求的方法是,首先為所有形參定義使用一個基類,然后為每一種特定類型的形參細分類這個基類。這種方法使得應用程序可以以基本形參定義類的實例數(shù)組的形式將形參定義提供給這個庫,而實際的定義則可以使用匹配每種形參類型的子類。對于業(yè)務計劃生成器的例子,這可以采用清單2中所示的形式:
清單 2. 業(yè)務計劃生成器的形參定義
private static final ParameterDef[] PARM_DEFS = { new BoolDef('c', "m_isConcise"),new IntDef('f', "m_initialRevenue", 10, 10000),new FloatDef('g', "m_growthRate", 1.0, 100.0),new StringDef('n', "m_productDescription") }有了得到允許的在一個數(shù)組中定義的形參,應用程序?qū)?shù)處理代碼的調(diào)用就可以像對一個靜態(tài)方法的單個調(diào)用一樣簡單。為了允許除形參數(shù)組中定義的實參之外額外的實參(要么是必需的值,要么是可變長度的值),我將令這個調(diào)用返回被處理實參的實際數(shù)量。這樣應用程序便可以檢查額外的實參并適當?shù)厥褂盟鼈儭W詈蟮慕Y(jié)果看上去如清單3所示:
清單 3. 使用庫
public class PlanGen {private static final ParameterDef[] PARM_DEFS = {...};public static void main(String[] args) {// if no arguments are supplied, assume help is neededif (args.length > 0) {// process arguments directly to instancePlanGen inst = new PlanGen();int next = ArgumentProcessor.processArgs(args, PARM_DEFS, inst);// next unused argument is output file nameif (next >= args.length) {System.err.println("Missing required output file name");System.exit(1);}File outf = new File(args[next++]);...} else {System.out.println("\nUsage: java PlanGen " +"[-options] file\nOptions are:\n c concise plan\n" +"f first year revenue (K$)\n g growth rate\n" +"n product description");}} }最后剩下的部分就是處理錯誤報告(例如一個未知的形參標記字符或者一個超出范圍的數(shù)字值)。出于這個目的,我將定義ArgumentErrorException?作為一個未經(jīng)檢查的異常,如果出現(xiàn)了某個這一類的錯誤,就將拋出這個異常。如果這個異常沒有被捕捉到,它將立即關(guān)閉應用程序,并將一條錯誤消息和棧跟蹤 輸出到控制臺。一個替代的方法是,您也可以在代碼中直接捕捉這個異常,并且用其他方式處理異常(例如,可能會與使用信息一起輸出真正的錯誤消息)。
回頁首
實現(xiàn)庫
為了讓這個庫像計劃的那樣使用反射,它需要查找由形參定義數(shù)組指定的一些字段,然后將適當?shù)闹荡娴竭@些來自相應的命令行參數(shù)的字段中。這項任務可以通過只查找實際的命令行參數(shù)所需的字段信息來處理,但是我反而選擇將查找和使用分開。我將預先找到所有的字段,然后 只使用在參數(shù)處理期間已經(jīng)被找到的信息。
預先找到所有的字段是一種防錯性編程的步驟,這樣做可以消除使用反射時帶來的一個潛在的問題。如果我只是查找需要的字段,那么就很容易破壞一個形參定義(例如,輸錯相應的字段名),而且還不能認識到有錯誤發(fā)生。這里不會有編譯時錯誤,因為字段名是作為?String?傳遞的,而且,只要命令行沒有指定與已破壞的形參定義相匹配的實參,程序也可以執(zhí)行得很好。這種被蒙蔽的錯誤很容易導致不完善代碼的發(fā)布。
假設(shè)我想在實際處理實參之前查找字段信息,清單4顯示了用于形參定義的基類的實現(xiàn),這個實現(xiàn)帶有一個?bindToClass()?方法,用于處理字段查找。
清單 4. 用于形參定義的基類
public abstract class ParameterDef {protected char m_char; // argument flag characterprotected String m_name; // parameter field nameprotected Field m_field; // actual parameter fieldprotected ParameterDef(char chr, String name) {m_char = chr;m_name = name;}public char getFlag() {return m_char;}protected void bindToClass(Class clas) {try {// handle the field look up and accessibilitym_field = clas.getDeclaredField(m_name);m_field.setAccessible(true);} catch (NoSuchFieldException ex) {throw new IllegalArgumentException("Field '" +m_name + "' not found in " + clas.getName());}}public abstract void handle(ArgumentProcessor proc); }實際的庫實現(xiàn)還涉及到本文沒有提及的幾個類。我不打算一一介紹每一個類,因為其中大部分類都與庫的反射方面不相關(guān)。我將提到的是,我選擇將目標對象存為?ArgumentProcessor?類的一個字段,并在這個類中實現(xiàn)一個形參字段的真正設(shè)置。這種方法為參數(shù)處理提供了一個簡單的模式:?ArgumentProcessor?類掃描實參以發(fā)現(xiàn)形參標記,為每個標記查找相應的形參定義(總是?ParameterDef?的一個子類),再調(diào)用這個定義的?handle()?方法。?handle()?方法在解釋完實參值之后,又調(diào)用?ArgumentProcessor?的?setValue()?方法。清單5顯示了ArgumentProcessor?類的不完整版本,包括在構(gòu)造函數(shù)中的形參綁定調(diào)用以及?setValue()?方法:
清單 5. 主庫類的部分清單
public class ArgumentProcessor {private Object m_targetObject; // parameter value objectprivate int m_currentIndex; // current argument position...public ArgumentProcessor(ParameterDef[] parms, Object target) {// bind all parameters to target classfor (int i = 0; i < parms.length; i++) {parms[i].bindToClass(target.getClass());}// save target object for later usem_targetObject = target;}public void setValue(Object value, Field field) {try {// set parameter field value using reflectionfield.set(m_targetObject, value);} catch (IllegalAccessException ex) {throw new IllegalArgumentException("Field " + field.getName() +" is not accessible in object of class " + m_targetObject.getClass().getName());}}public void reportArgumentError(char flag, String text) {throw new ArgumentErrorException(text + " for argument '" + flag + "' in argument " + m_currentIndex);}public static int processArgs(String[] args,ParameterDef[] parms, Object target) {ArgumentProcessor inst = new ArgumentProcessor(parms, target);...} }最后,清單6顯示了?int?形參值的形參定義子類的部分實現(xiàn)。這包括對基類的?bindToClass()?方法(來自?清單4)的重載,這個重載的方法首先調(diào)用基類的實現(xiàn),然后檢查找到的字段是否匹配預期的類型。其他特定形參類型(?boolean、float、String?,等等)的子類與此十分相似。
清單 6.?int?形參定義類
public class IntDef extends ParameterDef {private int m_min; // minimum allowed valueprivate int m_max; // maximum allowed valuepublic IntDef(char chr, String name, int min, int max) {super(chr, name);m_min = min;m_max = max;}protected void bindToClass(Class clas) {super.bindToClass(clas);Class type = m_field.getType();if (type != Integer.class && type != Integer.TYPE) {throw new IllegalArgumentException("Field '" + m_name +"'in " + clas.getName() + " is not of type int");}}public void handle(ArgumentProcessor proc) {// set up for validatingboolean minus = false;boolean digits = false;int value = 0;// convert number supplied in argument list to 'value'...// make sure we have a valid valuevalue = minus ? -value : value;if (!digits) {proc.reportArgumentError(m_char, "Missing value");} else if (value < m_min || value > m_max) {proc.reportArgumentError(m_char, "Value out of range");} else {proc.setValue(new Integer(value), m_field);}} }回頁首
結(jié)束庫
在本文中,我講述了一個用于處理命令行參數(shù)的庫的設(shè)計過程,作為反射的一個實際的例子。這個庫很好地闡明了如何有效地使用反射――它簡化應用程序的代碼,而且不用明顯地犧牲性能。犧牲了多少性能呢?從對我的開發(fā)系統(tǒng)的一些快速測試中可以看出,一個簡單的測試程序在使用整個庫進行了參數(shù)處理時比起不帶任何參數(shù)處理時運行起來平均只慢40毫秒。多出來的這些時間大部分是花在庫類和庫代碼所使用的其他類的裝載上,因此,即使是對于那些定義了許多命令行形參和許多實參值的應用程序,也不大可能會比這一結(jié)果糟很多。對于我的命令行應用程序,額外的40毫秒根本不能引起我的注意。
通過?參考資源中的鏈接可以找到完整的庫代碼。它包括我在本文沒有提到的一些特性,包括 這樣一些細節(jié),比如鉤子,用于容易地生成一列格式化的形參標記,還有一些描述,有助于為應用程序提供使用指令。歡迎您在自己的程序中使用這個庫,并以任何您發(fā)現(xiàn)有用的方式擴展這個庫。
現(xiàn)在我已講過了?第1部分中Java類的基礎(chǔ),也講過了?第2部分中的 Java Reflection API 的原理以及第3部分,本系列剩下的部分將改變話題,講講大家不大熟悉的字節(jié)碼處理。在第4部分,我將從容易的開始,先看看用于使用二進制類的用戶友好的 Javassist 庫。您是否想轉(zhuǎn)換方法,但是又不愿在字節(jié)碼中啟動程序呢?Javassist 正好適合您的需求。下個月我們將看看如何實現(xiàn)這一點。
原文:http://www.ibm.com/developerworks/cn/java/j-dyn0715/index.html
轉(zhuǎn)載于:https://www.cnblogs.com/davidwang456/p/4035654.html
總結(jié)
以上是生活随笔為你收集整理的Java 编程的动态性,第3部分: 应用反射--转载的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java编程 的动态性,第 2部分: 引
- 下一篇: Java 编程的动态性, 第4部分: 用