1. 程式人生 > >Java反射實體類屬性(get ,set方法)

Java反射實體類屬性(get ,set方法)

反射授予了你的程式碼訪問裝載進JVM內的Java類的內部資訊的許可權,並且允許你編寫在程式執行期間與所選擇的類的一同工作的程式碼,而不是在原始碼中。這種機制使得反射成為建立靈活的應用程式的強大工具,但是要小心的是,如果使用不恰當,反射會帶來很大的副作用。在這篇文章中,軟體諮詢顧問Dennis Sosnoski 介紹了反射的使用,同時還介紹了一些使用反射所要付出的代價。在這裡,你可以找到Java反射API是如何在執行時讓你鉤入物件的。

    在第一部分,我向你介紹了Java程式設計的類以及類的裝載。那篇文章中描述了很多出現在Java二進位制類格式中的資訊,現在我來介紹在執行時使用反射API訪問和使用這些資訊的基礎。為了使那些已經瞭解反射基礎的開發人員對這些事情感興趣,我還會介紹一些反射與直接訪問的在效能方面的比較。

    使用反射與和metadata(描述其它資料的資料)一些工作的Java程式設計是不同的。通過Java語言反射來訪問的元資料的特殊型別是在JVM內部的類和物件的描述。反射使你可以在執行時訪問各種類資訊,它甚至可以你讓在執行時讀寫屬性欄位、呼叫所選擇的類的方法。

    反射是一個強大的工具,它讓你建立靈活能夠在執行時組裝的程式碼,而不需要連線元件間的原始碼。反射的一些特徵也帶來一些問題。在這章中,我將會探究在應用程式中不打算使用反射的原因,以為什麼使用它的原因。在你瞭解到這些利弊之後,你就會在好處大於缺點的時候做出決定。

    初識class
    使用反射的起點總時一個java.lang.Class類的例項。如果你與一個預先確定的類一同工作,Java語言為直接獲得Class類的例項提供了一個簡單的快捷方式。例如:

    Class clas = MyClass.class;
    當你使用這項技術的時候,所有與裝載類有關的工作都發生在幕後。如果你需要在執行時從外部的資源中讀取類名,使用上面這種方法是不會達到目的的,相反你需要使用類裝載器來查詢類的資訊,方法如下所示:

    // "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

    如果類已經裝載,你將會找到當前在在的類的資訊。如果類還沒有被裝載,那麼類裝載器將會裝載它,並且返回最近建立的類的例項。

    關於類的反射
    Class物件給予你了所有的用於反射訪問類的元資料的基本鉤子。這些元資料包括有關類的自身資訊,例如象類的包和子類,還有這個類所實現的介面,還包括這個類所定義的構造器、屬性欄位以及方法的詳細資訊。後面的這些項是我們在程式設計過種經常使用的,因此在這一節的後面我會給出一些用這些資訊來工作的例子。

    對於類的構造中的每一種型別(構造器、屬性欄位、方法),java.lang.Class提供了四種獨立的反射呼叫以不的方式來訪問類的資訊。下面列出了這四種呼叫的標準形式,它是一組用於查詢構造器的呼叫。

    Constructor getConstructor(Class[] params)   使用指定的引數型別來獲得公共的構造器;
    Constructor[] getConstructors()    獲得這個類的所有構造器;
    Constructor getDeclaredConstructor(Class[] params) 使用指定的引數型別來獲得構造器(忽略訪問的級別)

    Constructor[] getDeclaredConstructors()  獲得這個類的所有的構造器(忽略訪問的級別)

    上述的每一種方法都返回一或多個java.lang.reflect.Constructor的例項。Constructor類定義了一個需要一個物件資料做為唯一引數的newInstance方法,然後返回一個最近建立的原始類的例項。物件陣列是在構造器呼叫時所使用的引數值。例如,假設你有一個帶有一對String 型別做為引數的構造器的TwoString類,程式碼如下所示:

     public class TwoString { private String m_s1, m_s2; public TwoString(String s1, String s2) { m_s1 = s1; m_s2 = s2; } }

 
    下面的程式碼顯示如何獲得TwoString類的構造器,並使用字串“a”和“b”來建立一個例項:

     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);

 
    上面的程式碼忽略了幾種可能的被不同的反射方法丟擲的異常檢查的型別。這些異常在JavadocAPI中有詳細的描述,因此為簡便起見,我會在所有的程式碼中忽略它們。

    在我涉及到構造器這個主題時,Java語言也定義了一個特殊的沒有引數的(或預設)構造器快捷方法,你能使用它來建立一個類的例項。這個快捷方法象下面的程式碼這樣被嵌入到類的自定義中:

    Object newInstance() ?使用預設的構造器建立新的例項。

    儘管這種方法只讓你使用一個特殊的構造器,但是如果你需要的話,它是非常便利的快捷方式。這項技術在使用JavaBeans工作的時候尤其有用,因為JavaBeans需要定義一個公共的、沒有引數的構造器。

    通過反射來查詢屬性欄位
    Class類反射呼叫訪問屬性欄位資訊與那些用於訪問構造器的方法類似,在有陣列型別的引數的使用屬性欄位名來替代:使用方法如下所示:

    Field getField(String name)  --獲得由name指定的具有public級別的屬性欄位
    Field getFields() ?獲得一個類的所有具有public級別的屬性欄位
    Field getDeclaredField(String name) ?獲得由name指定的被類宣告的屬性欄位
    Field getDeclaredFields() ?獲得由類定義的所有的屬性欄位
    儘管與構造器的呼叫很相似,但是在提到屬性欄位的時候,有一個重要的差別:前兩個方法返回能過類來訪問的公共(public)屬性欄位的資訊(包括那些來自於超類的屬性欄位),後兩個方法返回由類直接宣告的所有的屬性欄位(忽略了屬性欄位的訪問型別)。

    Java.lang.reflect.Field的例項通過呼叫定義好的getXXX和setXXX方法來返回所有的原始的資料型別,就像普通的與物件引用一起工作的get和set方法一樣。儘管getXXX方法會自動地處理資料型別轉換(例如使用getInt方法來獲取一個byte型別的值),但使用一個適當基於實際的屬性欄位型別的方法是應該優先考慮的。

    下面的程式碼顯示瞭如何使用屬性欄位的反射方法,通過指定屬性欄位名,找到一個物件的int型別的屬性欄位,並給這個屬性欄位值加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; }

 
    這個方法開始展現一些使用反射所可能帶來的靈活性,它優於與一個特定的類一同工作,incrementField方法把要查詢的類資訊的物件傳遞給getClass方法,然後直接在那個類中查詢命名的屬性欄位。

    通過反射來查詢方法
    Class反射呼叫訪問方法的資訊與訪問構造器和欄位屬性的方法非常相似:

    Method getMethod(String name,Class[] params)  --使用指定的引數型別獲得由name引數指定的public型別的方法。

    Mehtod[] getMethods()?獲得一個類的所有的public型別的方法
    Mehtod getDeclaredMethod(String name, Class[] params)?使用指定的引數型別獲得由name引數所指定的由這個類宣告的方法。

    Method[] getDeclaredMethods() ?獲得這個類所宣告的所有的方法
    與屬性欄位的呼叫一樣,前兩個方法返回通過這個類的例項可以訪問的public型別的方法?包括那些繼承於超類的方法。後兩個方法返回由這個類直接宣告的方法的資訊,而不管方法的訪問型別。

    通過呼叫返回的Java.lang.reflect.Mehtod例項定義了一個invoke方法,你可以使用它來呼叫定義類的有關例項。這個invoke方法需要兩個引數,一個是提供這個方法的類的例項,一個是呼叫這個方法所需要的引數值的陣列。

    下面給出了比屬性欄位的例子更加深入的例子,它顯示了一個的方法反射的例子,這個方法使用get和set方法來給JavaBean定義的int型別的屬性做增量操作。例如,如果物件為一個整數型別count屬性定義了getCount和setCount方法,那麼為了給這個屬性做增量運算,你就可以把“count”做為引數名傳遞給呼叫的這個方法中。示例程式碼如下:

    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; }

 
    根據JavaBeans的規範,我把屬性名的第一個字母轉換為大寫,然後在前面加上“get”來建立讀取屬性值的方法名,在屬性名前加上“set”來建立設定屬性值的方法名。JavaBeans的讀方法只返回屬性值,寫方法只需要要寫入的值做為引數,因此我指定了與這個方法相匹配的引數型別。最後規範規定這兩個方法應該是public型別的,因此我使用了查詢相關類的public型別方法的呼叫形式。

    這個例子我首先使用反射傳遞一個原始型別的值,因此讓我們來看一下它是怎樣工作的。基本的原理是簡單的:無論什麼時候,你需要傳遞一個原始型別的值,你只要替換相應的封裝原始型別的(在java.lang 包中定義的)的類的例項就可以了。這種方法可應用於呼叫和返回。因此在我的例子中呼叫get方法時,我預期的結果是一個由java.lang.Integer類所封裝的實際的int型別的屬性值。

    反射陣列
    在Java語言中陣列是物件,象其它所有的物件一樣,它有一些類。如果你有一個數組,你可以和其它任何物件一樣使用標準的getClass方法來獲得這個陣列的類,但是你獲得的這個類與其它的物件型別相比,不同之處在它沒有一個現存的工作例項。即使你有了一個數組類之後,你也不能夠直接用它來做任何事情,因為通過反射為普通的類所提供的構造器訪問不能為陣列工作,並且陣列沒有任何可訪問的屬性欄位,只有基本的為陣列物件定義的java.lang.Object型別的方法。

    陣列特殊處理要使用java.lang.reflect.Array類提供的一個靜態方法的集合,這個類中的方法可以讓你建立新的陣列,獲得一個數組物件的長度,以及讀寫一個數組物件的索引值。

    下面的程式碼顯示了有效調整一個現存陣列的尺寸的方法。它使用反射來建立一個相同型別的新陣列,然後在返回這個新陣列之前把原陣列中的所有的資料複製到新的陣列中。

    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; }

安全與反射
    在處理反射的時候,安全是一個複雜的問題。反射正常被框架型別的程式碼使用,並因為這樣,你可能會經常要求框架不關心普通的訪問限制來完全訪問你的程式碼。然而,自由的訪問可能會在其它的一些例項中產生一些風險,例如在程式碼在一個不被信任的程式碼共享環境中被執行的時候。

    因為這些衝突的需要,Java語言定義了一個多級方法來處理反射安全。基本的模式是在反射請求原始碼訪問的時候強制使用如下相同的約束限制:

    訪問這個類中來自任何地方的public元件;
    不訪問這個類本身外部的private元件;
    限制訪問protected和package(預設訪問)元件。

    圍繞這些限制有一個簡單的方法,我在前面的例子中所使用的所有構造器、屬性欄位、以及類的方法都擴充套件於一個共同的基類???java.lang.reflect.AccessibleObject類。這個類定義了一個setAccessible方法,這個方法可以讓你開啟或關閉這些對類的例項的訪問檢查。如果安全管理器被設定為關閉訪問檢查,那麼就允許你訪問,否則不允許,安全管理器會丟擲一個異常。

    下面是一個使用反向來演示這種行為的TwoString類的例項。

    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); } } }

 
    如果你編譯這段程式碼並且直接使用不帶任何引數的命令列命令來執行這個程式,它會丟擲一個關於field.get(inst)呼叫的IllegalAccessException異常,如果你去掉上面程式碼中field.setAccessible(true)行的註釋,然後編譯並重新執行程式碼,它就會成功執行。最後,如果你在命令列給JVM新增一個Djava.security.manager引數,使得安全管理器可用,那麼它又會失敗,除非你為ReflectSecurity類定義安全許可。

    反射效能
    反射是一個強大的工具,但是也會帶一些缺點。主要缺點之一就是對效能的影響。使用反射是基本的解釋性操作,你告訴JVM你要做什麼,它就會為你做什麼。這種操作型別總是比直接做同樣的操作要慢。為了演示使用反射所要付出的效能代價,我為這篇文章準備了一套基準程式(可以從資源中下載)。

    下面列出一段來自於屬性欄位的訪問效能測試的摘要,它包括基本的測試方法。每個方法測試一種訪問屬性欄位的形式,accessSame方法和本物件的成員欄位一起工作,accessReference方法直接使用另外的物件屬性欄位來存取,accessReflection通過反射使用另一個物件的屬性欄位來存取,每個方法都使用相同的計算???在迴圈中簡單的加/乘運算。

     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; } }

 
    測試程式在一個大迴圈中反覆的呼叫每個方法,在呼叫結束後計算平均時間。每個方法的第一次呼叫不包括在平均值中,因些初始化時間不是影響結果的因素。為這篇文章所做的測試執行,我為每個呼叫使用了10000000的迴圈計數,程式碼執行在1GHzPIII系統上。並且分別使用了三個不同的LinuxJVM,對於每個JVM都使用了預設設定,測試結果如下圖所示:

    上面的圖表的刻度可以顯示整個測試範圍,但是那樣的話就會減少差別的顯示效果。這個圖表中的前兩個是用SUN的JVM的進行測試的結果圖,使用反射的執行時間比使用直接訪問的時間要超過1000多倍。最後一個圖是用IBM的JVM所做的測試,通過比較要SUN的JVM執行效率要高一些,但是使用反射的方法依然要比其它方法超出700多倍。雖然IBM的JVM要比SUN的JVM幾乎要快兩倍,但是在使用反射之外的兩種方法之間,對於任何的JVM在執行效率上沒有太大的差別。最大的可能是,這種差別反映了通過Sun Hot Spot JVMs在簡化基準方面所做的專門優化很少。

    除了屬性欄位訪問時間的測試以外,我對方法做了同樣的測試。對於方法的呼叫,我償試了與屬性欄位訪問測試一樣的三種方式,用額外使用了沒有引數的方法的變數與傳遞並返回一個值的方法呼叫相對比。下面的程式碼顯示了使用傳遞並返回值的呼叫方式進行測試的三種方法。

     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; } }


    下圖顯示我使用這些方法的測試結果,這裡再一次顯示了反射要比其它的直接訪問要慢很多。雖然對於無引數的案例,執行效率從SUN1.3.1JVM的慢幾百倍到IBM的JVM慢不到30倍,與屬性欄位訪問案例相比,差別不是很大,這種情況的部分原因是因為java.lang.Integer的包裝器需要傳遞和返回int型別的值。因為Intergers是不變的,因此就需要為每個方法的返回生成一個新值,這就增加了相當大的系統開銷。

    反射的效能是SUN在開發1.4JVM時重點關注的一個領域,從上圖可以看到改善的結果。Sun1.4.1JVM對於這種型別的操作比1.3.1版有了很大的提高,要我的測試中要快大約7倍。IBM的1.4.0JVM對於這種測試提供了更好的效能,它的執行效率要比Sun1.4.1JVM快兩到三倍。

    我還為使用反射建立物件編寫了一個類似的效率測試程式。雖然這個例子與屬性欄位和方法呼叫相比差別不是很大,但是在Sun1.3.1JVM上呼叫newInstance()方法建立一個簡單的java.lang.Object大約比直接使用new Object()方法長12倍的時間,在IBM1.4.0JVM上大約要長4倍的時間,在Sun1.4.1JVM上大約要長2倍的時間。對於任何用於測試的JVM,使用Array.newInstance(Type,size)方法建立一個數組所需要的時間比使用new tye[size]所花費的時間大約要長兩倍,隨著陣列民尺寸的增長,這兩種方法的差別的將隨之減少。

    反射概要總結
    Java 語言的反射機制提供了一種非常通用的動態連線程式元件的方法。它允許你的程式建立和維護任何類的物件(服從安全限制),而不需要提前對目標類進行硬編碼。這些特徵使得反射在建立與物件一同工作的類庫中的通用方法方面非常有用。例如,反射經常被用於那些資料庫,XML、或者其它的外部的持久化物件的框架中。

    反射還有兩個缺點,一個是效能問題。在使用屬性欄位和方法訪問的時候,反射要比直接的程式碼訪問要慢很多。至於對影響的程度,依賴於在程式中怎樣使用反射。如果它被用作一個相關的很少發生的程式操作中,那麼就不必關心降低的效能,即使在我的測試中所展示的最耗時的反射操作的圖形中也只是幾微秒的時間。如果要在執行應用程式的核心邏輯中使用反射,效能問題才成為一個要嚴肅物件的問題。

    對於很多應用中的存在的缺點是使用反射可以使你的實際的程式碼內部邏輯變得模糊不清。程式設計師都希望在原始碼中看到一個程式的邏輯以及象繞過原始碼的反射所可能產生的維護問題這樣的一些技術。反射程式碼也比相應的直接程式碼要複雜一些,就像在效能比較的程式碼例項看到那樣。處理這些問題的最好方法是儘可能少使用反射,只有在一些增加靈活性的地方來使用它。

    在下一篇文章中,我將給出一個更加詳細的如何使用反射的例子。這個例子提供了一個用於處理傳遞給一個Java應用程式的命令列引數的API。在避免弱點的同時,它也顯示了反射的強大的功能,反射能夠使用的你的命令處理變得的簡單嗎?你可以在Java 動態程式設計的第三部分中找到答案。