Java動態(tài)程序設(shè)計(jì):反射介紹 使用運(yùn)行的類的信息使你的程序設(shè)計(jì)更加靈活
反射授予了你的代碼訪問裝載進(jìn)JVM內(nèi)的Java類的內(nèi)部信息的權(quán)限,并且允許你編寫在程序執(zhí)行期間與所選擇的類的一同工作的代碼,而不是在源代碼中。這種機(jī)制使得反射成為創(chuàng)建靈活的應(yīng)用程序的強(qiáng)大工具,但是要小心的是,如果使用不恰當(dāng),反射會帶來很大的副作用。在這篇文章中,軟件咨詢顧問Dennis Sosnoski 介紹了反射的使用,同時(shí)還介紹了一些使用反射所要付出的代價(jià)。在這里,你可以找到Java反射API是如何在運(yùn)行時(shí)讓你鉤入對象的。
在第一部分,我向你介紹了Java程序設(shè)計(jì)的類以及類的裝載。那篇文章中描述了很多出現(xiàn)在Java二進(jìn)制類格式中的信息,現(xiàn)在我來介紹在運(yùn)行時(shí)使用反射API訪問和使用這些信息的基礎(chǔ)。為了使那些已經(jīng)了解反射基礎(chǔ)的開發(fā)人員對這些事情感興趣,我還會介紹一些反射與直接訪問的在性能方面的比較。
使用反射與和metadata(描述其它數(shù)據(jù)的數(shù)據(jù))一些工作的Java程序設(shè)計(jì)是不同的。通過Java語言反射來訪問的元數(shù)據(jù)的特殊類型是在JVM內(nèi)部的類和對象的描述。反射使你可以在運(yùn)行時(shí)訪問各種類信息,它甚至可以你讓在運(yùn)行時(shí)讀寫屬性字段、調(diào)用所選擇的類的方法。
反射是一個(gè)強(qiáng)大的工具,它讓你建立靈活能夠在運(yùn)行時(shí)組裝的代碼,而不需要連接組件間的源代碼。反射的一些特征也帶來一些問題。在這章中,我將會探究在應(yīng)用程序中不打算使用反射的原因,以為什么使用它的原因。在你了解到這些利弊之后,你就會在好處大于缺點(diǎn)的時(shí)候做出決定。
初識class 使用反射的起點(diǎn)總時(shí)一個(gè)java.lang.Class類的實(shí)例。如果你與一個(gè)預(yù)先確定的類一同工作,Java語言為直接獲得Class類的實(shí)例提供了一個(gè)簡單的快捷方式。例如: Class clas = MyClass.class;
當(dāng)你使用這項(xiàng)技術(shù)的時(shí)候,所有與裝載類有關(guān)的工作都發(fā)生在幕后。如果你需要在運(yùn)行時(shí)從外部的資源中讀取類名,使用上面這種方法是不會達(dá)到目的的,相反你需要使用類裝載器來查找類的信息,方法如下所示: // "name" is the class name to load Class clas = null; try { clas = Class.forName(name); } catch (ClassNotFoundException ex) { // handle exception case } // use the loaded class
如果類已經(jīng)裝載,你將會找到當(dāng)前在在的類的信息。如果類還沒有被裝載,那么類裝載器將會裝載它,并且返回最近創(chuàng)建的類的實(shí)例。
關(guān)于類的反射
Class對象給予你了所有的用于反射訪問類的元數(shù)據(jù)的基本鉤子。這些元數(shù)據(jù)包括有關(guān)類的自身信息,例如象類的包和子類,還有這個(gè)類所實(shí)現(xiàn)的接口,還包括這個(gè)類所定義的構(gòu)造器、屬性字段以及方法的詳細(xì)信息。后面的這些項(xiàng)是我們在程序設(shè)計(jì)過種經(jīng)常使用的,因此在這一節(jié)的后面我會給出一些用這些信息來工作的例子。
對于類的構(gòu)造中的每一種類型(構(gòu)造器、屬性字段、方法),java.lang.Class提供了四種獨(dú)立的反射調(diào)用以不的方式來訪問類的信息。下面列出了這四種調(diào)用的標(biāo)準(zhǔn)形式,它是一組用于查找構(gòu)造器的調(diào)用。
Constructor getConstructor(Class[] params) 使用指定的參數(shù)類型來獲得公共的構(gòu)造器; Constructor[] getConstructors() 獲得這個(gè)類的所有構(gòu)造器; Constructor getDeclaredConstructor(Class[] params) 使用指定的參數(shù)類型來獲得構(gòu)造器(忽略訪問的級別) Constructor[] getDeclaredConstructors() 獲得這個(gè)類的所有的構(gòu)造器(忽略訪問的級別)
上述的每一種方法都返回一或多個(gè)java.lang.reflect.Constructor的實(shí)例。Constructor類定義了一個(gè)需要一個(gè)對象數(shù)據(jù)做為唯一參數(shù)的newInstance方法,然后返回一個(gè)最近創(chuàng)建的原始類的實(shí)例。對象數(shù)組是在構(gòu)造器調(diào)用時(shí)所使用的參數(shù)值。例如,假設(shè)你有一個(gè)帶有一對String 類型做為參數(shù)的構(gòu)造器的TwoString類,代碼如下所示: public class TwoString { private String m_s1, m_s2; public TwoString(String s1, String s2) { m_s1 = s1; m_s2 = s2; } }
下面的代碼顯示如何獲得TwoString類的構(gòu)造器,并使用字符串“a”和“b”來創(chuàng)建一個(gè)實(shí)例: Class[] types = new Class[] { String.class, String.class }; Constructor cons = TwoString.class.getConstructor(types); Object[] args = new Object[] { "a", "b" }; TwoString ts = cons.newInstance(args);
上面的代碼忽略了幾種可能的被不同的反射方法拋出的異常檢查的類型。這些異常在Javadoc API中有詳細(xì)的描述,因此為簡便起見,我會在所有的代碼中忽略它們。
在我涉及到構(gòu)造器這個(gè)主題時(shí),Java語言也定義了一個(gè)特殊的沒有參數(shù)的(或默認(rèn))構(gòu)造器快捷方法,你能使用它來創(chuàng)建一個(gè)類的實(shí)例。這個(gè)快捷方法象下面的代碼這樣被嵌入到類的自定義中: Object newInstance() ?使用默認(rèn)的構(gòu)造器創(chuàng)建新的實(shí)例。
盡管這種方法只讓你使用一個(gè)特殊的構(gòu)造器,但是如果你需要的話,它是非常便利的快捷方式。這項(xiàng)技術(shù)在使用JavaBeans工作的時(shí)候尤其有用,因?yàn)镴avaBeans需要定義一個(gè)公共的、沒有參數(shù)的構(gòu)造器。
通過反射來查找屬性字段
Class類反射調(diào)用訪問屬性字段信息與那些用于訪問構(gòu)造器的方法類似,在有數(shù)組類型的參數(shù)的使用屬性字段名來替代:使用方法如下所示: Field getField(String name) --獲得由name指定的具有public級別的屬性字段 Field getFields() ?獲得一個(gè)類的所有具有public級別的屬性字段 Field getDeclaredField(String name) ?獲得由name指定的被類聲明的屬性字段 Field getDeclaredFields() ?獲得由類定義的所有的屬性字段
盡管與構(gòu)造器的調(diào)用很相似,但是在提到屬性字段的時(shí)候,有一個(gè)重要的差別:前兩個(gè)方法返回能過類來訪問的公共(public)屬性字段的信息(包括那些來自于超類的屬性字段),后兩個(gè)方法返回由類直接聲明的所有的屬性字段(忽略了屬性字段的訪問類型)。
Java.lang.reflect.Field的實(shí)例通過調(diào)用定義好的getXXX和setXXX方法來返回所有的原始的數(shù)據(jù)類型,就像普通的與對象引用一起工作的get和set方法一樣。盡管getXXX方法會自動地處理數(shù)據(jù)類型轉(zhuǎn)換(例如使用getInt方法來獲取一個(gè)byte類型的值),但使用一個(gè)適當(dāng)基于實(shí)際的屬性字段類型的方法是應(yīng)該優(yōu)先考慮的。
下面的代碼顯示了如何使用屬性字段的反射方法,通過指定屬性字段名,找到一個(gè)對象的int類型的屬性字段,并給這個(gè)屬性字段值加1。 public int incrementField(String name, Object obj) throws... { Field field = obj.getClass().getDeclaredField(name); int value = field.getInt(obj) + 1; field.setInt(obj, value); return value; }
這個(gè)方法開始展現(xiàn)一些使用反射所可能帶來的靈活性,它優(yōu)于與一個(gè)特定的類一同工作,incrementField方法把要查找的類信息的對象傳遞給getClass方法,然后直接在那個(gè)類中查找命名的屬性字段。
通過反射來查找方法 Class反射調(diào)用訪問方法的信息與訪問構(gòu)造器和字段屬性的方法非常相似: Method getMethod(String name,Class[] params) --使用指定的參數(shù)類型獲得由name參數(shù)指定的public類型的方法。 Mehtod[] getMethods()?獲得一個(gè)類的所有的public類型的方法 Mehtod getDeclaredMethod(String name, Class[] params)?使用指定的參數(shù)類型獲得由name參數(shù)所指定的由這個(gè)類聲明的方法。 Method[] getDeclaredMethods() ?獲得這個(gè)類所聲明的所有的方法
與屬性字段的調(diào)用一樣,前兩個(gè)方法返回通過這個(gè)類的實(shí)例可以訪問的public類型的方法?包括那些繼承于超類的方法。后兩個(gè)方法返回由這個(gè)類直接聲明的方法的信息,而不管方法的訪問類型。
通過調(diào)用返回的Java.lang.reflect.Mehtod實(shí)例定義了一個(gè)invoke方法,你可以使用它來調(diào)用定義類的有關(guān)實(shí)例。這個(gè)invoke方法需要兩個(gè)參數(shù),一個(gè)是提供這個(gè)方法的類的實(shí)例,一個(gè)是調(diào)用這個(gè)方法所需要的參數(shù)值的數(shù)組。
下面給出了比屬性字段的例子更加深入的例子,它顯示了一個(gè)的方法反射的例子,這個(gè)方法使用get和set方法來給JavaBean定義的int類型的屬性做增量操作。例如,如果對象為一個(gè)整數(shù)類型count屬性定義了getCount和setCount方法,那么為了給這個(gè)屬性做增量運(yùn)算,你就可以把“count”做為參數(shù)名傳遞給調(diào)用的這個(gè)方法中。示例代碼如下: public int incrementProperty(String name, Object obj) { String prop = Character.toUpperCase(name.charAt(0)) + name.substring(1); String mname = "get" + prop; Class[] types = new Class[] {}; Method method = obj.getClass().getMethod(mname, types); Object result = method.invoke(obj, new Object[0]); int value = ((Integer)result).intValue() + 1; mname = "set" + prop; types = new Class[] { int.class }; method = obj.getClass().getMethod(mname, types); method.invoke(obj, new Object[] { new Integer(value) }); return value; }
根據(jù)JavaBeans的規(guī)范,我把屬性名的第一個(gè)字母轉(zhuǎn)換為大寫,然后在前面加上“get”來建立讀取屬性值的方法名,在屬性名前加上“set”來建立設(shè)置屬性值的方法名。JavaBeans的讀方法只返回屬性值,寫方法只需要要寫入的值做為參數(shù),因此我指定了與這個(gè)方法相匹配的參數(shù)類型。最后規(guī)范規(guī)定這兩個(gè)方法應(yīng)該是public類型的,因此我使用了查找相關(guān)類的public類型方法的調(diào)用形式。
這個(gè)例子我首先使用反射傳遞一個(gè)原始類型的值,因此讓我們來看一下它是怎樣工作的。基本的原理是簡單的:無論什么時(shí)候,你需要傳遞一個(gè)原始類型的值,你只要替換相應(yīng)的封裝原始類型的(在java.lang 包中定義的)的類的實(shí)例就可以了。這種方法可應(yīng)用于調(diào)用和返回。因此在我的例子中調(diào)用get方法時(shí),我預(yù)期的結(jié)果是一個(gè)由java.lang.Integer類所封裝的實(shí)際的int類型的屬性值。
反射數(shù)組
在Java語言中數(shù)組是對象,象其它所有的對象一樣,它有一些類。如果你有一個(gè)數(shù)組,你可以和其它任何對象一樣使用標(biāo)準(zhǔn)的getClass方法來獲得這個(gè)數(shù)組的類,但是你獲得的這個(gè)類與其它的對象類型相比,不同之處在它沒有一個(gè)現(xiàn)存的工作實(shí)例。即使你有了一個(gè)數(shù)組類之后,你也不能夠直接用它來做任何事情,因?yàn)橥ㄟ^反射為普通的類所提供的構(gòu)造器訪問不能為數(shù)組工作,并且數(shù)組沒有任何可訪問的屬性字段,只有基本的為數(shù)組對象定義的java.lang.Object類型的方法。
數(shù)組特殊處理要使用java.lang.reflect.Array類提供的一個(gè)靜態(tài)方法的集合,這個(gè)類中的方法可以讓你創(chuàng)建新的數(shù)組,獲得一個(gè)數(shù)組對象的長度,以及讀寫一個(gè)數(shù)組對象的索引值。
下面的代碼顯示了有效調(diào)整一個(gè)現(xiàn)存數(shù)組的尺寸的方法。它使用反射來創(chuàng)建一個(gè)相同類型的新數(shù)組,然后在返回這個(gè)新數(shù)組之前把原數(shù)組中的所有的數(shù)據(jù)復(fù)制到新的數(shù)組中。 public Object growArray(Object array, int size) { Class type = array.getClass().getComponentType(); Object grown = Array.newInstance(type, size); System.arraycopy(array, 0, grown, 0, Math.min(Array.getLength(array), size)); return grown; }
安全與反射
在處理反射的時(shí)候,安全是一個(gè)復(fù)雜的問題。反射正常被框架類型的代碼使用,并因?yàn)檫@樣,你可能會經(jīng)常要求框架不關(guān)心普通的訪問限制來完全訪問你的代碼。然而,自由的訪問可能會在其它的一些實(shí)例中產(chǎn)生一些風(fēng)險(xiǎn),例如在代碼在一個(gè)不被信任的代碼共享環(huán)境中被執(zhí)行的時(shí)候。
因?yàn)檫@些沖突的需要,Java語言定義了一個(gè)多級方法來處理反射安全。基本的模式是在反射請求源碼訪問的時(shí)候強(qiáng)制使用如下相同的約束限制: 訪問這個(gè)類中來自任何地方的public組件; 不訪問這個(gè)類本身外部的private組件; 限制訪問protected和package(默認(rèn)訪問)組件。
圍繞這些限制有一個(gè)簡單的方法,我在前面的例子中所使用的所有構(gòu)造器、屬性字段、以及類的方法都擴(kuò)展于一個(gè)共同的基類???java.lang.reflect.AccessibleObject類。這個(gè)類定義了一個(gè)setAccessible方法,這個(gè)方法可以讓你打開或關(guān)閉這些對類的實(shí)例的訪問檢查。如果安全管理器被設(shè)置為關(guān)閉訪問檢查,那么就允許你訪問,否則不允許,安全管理器會拋出一個(gè)異常。
下面是一個(gè)使用反向來演示這種行為的TwoString類的實(shí)例。 public class ReflectSecurity { public static void main(String[] args) { try { TwoString ts = new TwoString("a", "b"); Field field = clas.getDeclaredField("m_s1"); // field.setAccessible(true); System.out.println("Retrieved value is " + field.get(inst)); } catch (Exception ex) { ex.printStackTrace(System.out); } } }
如果你編譯這段代碼并且直接使用不帶任何參數(shù)的命令行命令來運(yùn)行這個(gè)程序,它會拋出一個(gè)關(guān)于field.get(inst)調(diào)用的IllegalAccessException異常,如果你去掉上面代碼中field.setAccessible(true)行的注釋,然后編譯并重新運(yùn)行代碼,它就會成功執(zhí)行。最后,如果你在命令行給JVM添加一個(gè)Djava.security.manager參數(shù),使得安全管理器可用,那么它又會失敗,除非你為ReflectSecurity類定義安全許可。
反射性能 反射是一個(gè)強(qiáng)大的工具,但是也會帶一些缺點(diǎn)。主要缺點(diǎn)之一就是對性能的影響。使用反射是基本的解釋性操作,你告訴JVM你要做什么,它就會為你做什么。這種操作類型總是比直接做同樣的操作要慢。為了演示使用反射所要付出的性能代價(jià),我為這篇文章準(zhǔn)備了一套基準(zhǔn)程序(可以從資源中下載)。
下面列出一段來自于屬性字段的訪問性能測試的摘要,它包括基本的測試方法。每個(gè)方法測試一種訪問屬性字段的形式,accessSame方法和本對象的成員字段一起工作,accessReference方法直接使用另外的對象屬性字段來存取,accessReflection通過反射使用另一個(gè)對象的屬性字段來存取,每個(gè)方法都使用相同的計(jì)算???在循環(huán)中簡單的加/乘運(yùn)算。 public int accessSame(int loops) { m_value = 0; for (int index = 0; index < loops; index++) { m_value = (m_value + ADDITIVE_VALUE) * MULTIPLIER_VALUE; } return m_value; }
public int accessReference(int loops) { TimingClass timing = new TimingClass(); for (int index = 0; index < loops; index++) { timing.m_value = (timing.m_value + ADDITIVE_VALUE) * MULTIPLIER_VALUE; } return timing.m_value; }
public int accessReflection(int loops) throws Exception { TimingClass timing = new TimingClass(); try { Field field = TimingClass.class. getDeclaredField("m_value"); for (int index = 0; index < loops; index++) { int value = (field.getInt(timing) + ADDITIVE_VALUE) * MULTIPLIER_VALUE; field.setInt(timing, value); } return timing.m_value; } catch (Exception ex) { System.out.println("Error using reflection"); throw ex; } }
測試程序在一個(gè)大循環(huán)中反復(fù)的調(diào)用每個(gè)方法,在調(diào)用結(jié)束后計(jì)算平均時(shí)間。每個(gè)方法的第一次調(diào)用不包括在平均值中,因些初始化時(shí)間不是影響結(jié)果的因素。為這篇文章所做的測試運(yùn)行,我為每個(gè)調(diào)用使用了10000000的循環(huán)計(jì)數(shù),代碼運(yùn)行在1GHz PIII系統(tǒng)上。并且分別使用了三個(gè)不同的Linux JVM,對于每個(gè)JVM都使用了默認(rèn)設(shè)置,測試結(jié)果如下圖所示:
上面的圖表的刻度可以顯示整個(gè)測試范圍,但是那樣的話就會減少差別的顯示效果。這個(gè)圖表中的前兩個(gè)是用SUN的JVM的進(jìn)行測試的結(jié)果圖,使用反射的執(zhí)行時(shí)間比使用直接訪問的時(shí)間要超過1000多倍。最后一個(gè)圖是用IBM的JVM所做的測試,通過比較要SUN的JVM執(zhí)行效率要高一些,但是使用反射的方法依然要比其它方法超出700多倍。雖然IBM的JVM要比SUN的JVM幾乎要快兩倍,但是在使用反射之外的兩種方法之間,對于任何的JVM在執(zhí)行效率上沒有太大的差別。最大的可能是,這種差別反映了通過Sun Hot Spot JVMs在簡化基準(zhǔn)方面所做的專門優(yōu)化很少。
除了屬性字段訪問時(shí)間的測試以外,我對方法做了同樣的測試。對于方法的調(diào)用,我償試了與屬性字段訪問測試一樣的三種方式,用額外使用了沒有參數(shù)的方法的變量與傳遞并返回一個(gè)值的方法調(diào)用相對比。下面的代碼顯示了使用傳遞并返回值的調(diào)用方式進(jìn)行測試的三種方法。 public int callDirectArgs(int loops) { int value = 0; for (int index = 0; index < loops; index++) { value = step(value); } return value; }
public int callReferenceArgs(int loops) { TimingClass timing = new TimingClass(); int value = 0; for (int index = 0; index < loops; index++) { value = timing.step(value); } return value; }
public int callReflectArgs(int loops) throws Exception { TimingClass timing = new TimingClass(); try { Method method = TimingClass.class.getMethod ("step", new Class [] { int.class }); Object[] args = new Object[1]; Object value = new Integer(0); for (int index = 0; index < loops; index++) { args[0] = value; value = method.invoke(timing, args); } return ((Integer)value).intValue(); } catch (Exception ex) { System.out.println("Error using reflection"); throw ex; } }
下圖顯示我使用這些方法的測試結(jié)果,這里再一次顯示了反射要比其它的直接訪問要慢很多。雖然對于無參數(shù)的案例,執(zhí)行效率從SUN1.3.1JVM的慢幾百倍到IBM的JVM慢不到30倍,與屬性字段訪問案例相比,差別不是很大,這種情況的部分原因是因?yàn)閖ava.lang.Integer的包裝器需要傳遞和返回int類型的值。因?yàn)镮ntergers是不變的,因此就需要為每個(gè)方法的返回生成一個(gè)新值,這就增加了相當(dāng)大的系統(tǒng)開銷。
反射的性能是SUN在開發(fā)1.4JVM時(shí)重點(diǎn)關(guān)注的一個(gè)領(lǐng)域,從上圖可以看到改善的結(jié)果。Sun1.4.1JVM對于這種類型的操作比1.3.1版有了很大的提高,要我的測試中要快大約7倍。IBM的1.4.0JVM對于這種測試提供了更好的性能,它的運(yùn)行效率要比Sun1.4.1JVM快兩到三倍。
我還為使用反射創(chuàng)建對象編寫了一個(gè)類似的效率測試程序。雖然這個(gè)例子與屬性字段和方法調(diào)用相比差別不是很大,但是在Sun1.3.1JVM上調(diào)用newInstance()方法創(chuàng)建一個(gè)簡單的java.lang.Object大約比直接使用new Object()方法長12倍的時(shí)間,在IBM1.4.0JVM上大約要長4倍的時(shí)間,在Sun1.4.1JVM上大約要長2倍的時(shí)間。對于任何用于測試的JVM,使用Array.newInstance(Type,size)方法創(chuàng)建一個(gè)數(shù)組所需要的時(shí)間比使用new tye[size]所花費(fèi)的時(shí)間大約要長兩倍,隨著數(shù)組民尺寸的增長,這兩種方法的差別的將隨之減少。
反射概要總結(jié)
Java 語言的反射機(jī)制提供了一種非常通用的動態(tài)連接程序組件的方法。它允許你的程序創(chuàng)建和維護(hù)任何類的對象(服從安全限制),而不需要提前對目標(biāo)類進(jìn)行硬編碼。這些特征使得反射在創(chuàng)建與對象一同工作的類庫中的通用方法方面非常有用。例如,反射經(jīng)常被用于那些數(shù)據(jù)庫,XML、或者其它的外部的持久化對象的框架中。
反射還有兩個(gè)缺點(diǎn),一個(gè)是性能問題。在使用屬性字段和方法訪問的時(shí)候,反射要比直接的代碼訪問要慢很多。至于對影響的程度,依賴于在程序中怎樣使用反射。如果它被用作一個(gè)相關(guān)的很少發(fā)生的程序操作中,那么就不必關(guān)心降低的性能,即使在我的測試中所展示的最耗時(shí)的反射操作的圖形中也只是幾微秒的時(shí)間。如果要在執(zhí)行應(yīng)用程序的核心邏輯中使用反射,性能問題才成為一個(gè)要嚴(yán)肅對象的問題。
對于很多應(yīng)用中的存在的缺點(diǎn)是使用反射可以使你的實(shí)際的代碼內(nèi)部邏輯變得模糊不清。程序員都希望在源代碼中看到一個(gè)程序的邏輯以及象繞過源代碼的反射所可能產(chǎn)生的維護(hù)問題這樣的一些技術(shù)。反射代碼也比相應(yīng)的直接代碼要復(fù)雜一些,就像在性能比較的代碼實(shí)例看到那樣。處理這些問題的最好方法是盡可能少使用反射,只有在一些增加靈活性的地方來使用它。
在下一篇文章中,我將給出一個(gè)更加詳細(xì)的如何使用反射的例子。這個(gè)例子提供了一個(gè)用于處理傳遞給一個(gè)Java應(yīng)用程序的命令行參數(shù)的API。在避免弱點(diǎn)的同時(shí),它也顯示了反射的強(qiáng)大的功能,反射能夠使用的你的命令處理變得的簡單嗎?你可以在Java 動態(tài)程序設(shè)計(jì)的第三部分中找到答案。
| |