使用反射不同于常規的Java編程,其中它與 元數據--描述其它數據的數據協作。Java語言反射接入的特殊類型的原數據是JVM中類和對象的描述。反射使您能夠運行時接入廣泛的類信息。它甚至使您能夠讀寫字段,調用運行時選擇的類的方法。
反射是一種強大的工具。它使您能夠創建靈活的代碼,這些代碼可以在運行時裝配,無需在組件之間進行源代表鏈接。但反射的某些方面存在一些疑問。在本文中,我將深入討論為什么您可能 不希望在程序中使用反射,以及您應該這樣做的理由。在了解了權衡性分析之后,您可以自行決定是否利大于弊。
初學者的類
使用反射的啟點總是 java.lang.Class 實例。如果您希望與預先定義的類協作,那么Java語言提供一種直接獲得 Class 實例的簡便快捷方式:
當您使用這一項技術時,裝入類涉及的所有工作在幕后進行。但是,如果您需要在運行時從某些外部源讀取類名,這種方法并不適合。實際上,您需要使用一個類裝入器來查找類信息。以下介紹一種方法:
如果已經裝入了類,您將得到現有的 Class 信息。如果類未被裝入,類裝入器將現在裝入并返回新創建的類實例。
Class 對象為您提供接入類元數據的反射的所有基本hook。這類元數據包括關于類自身的信息,如包和類的父類,以及該類實施的接口。它還包括該類定義的構造函數、字段和方法的詳細信息。這些最后的項目都是編程中最經常使用的項目, 因此我將在本小節的稍后部分給出一些與它們協作的實例。
對于以下三類組件中的任何一類來說 -- 構造函數、字段和方法 -- java.lang.Class 提供四種獨立的反射調用,以不同的方式來獲得信息。調用都遵循一種標準格式。以下是用于查找構造函數的一組反射調用:
Constructor getConstructor(Class[] params) -- 獲得使用特殊的參數類型的公共構造函數,
Constructor[] getConstructors() -- 獲得類的所有公共構造函數
Constructor getDeclaredConstructor(Class[] params) -- 獲得使用特定參數類型的構造函數(與接入級別無關)
Constructor[] getDeclaredConstructors() -- 獲得類的所有構造函數(與接入級別無關)
每類這些調用都返回一個或多個 java.lang.reflect.Constructor 函數。這種 Constructor 類定義 newInstance 方法,它采用一組對象作為其唯一的參數,然后返回新創建的原始類實例。該組對象是用于構造函數調用的參數值。作為解釋這一工作流程的實例,假設您有一個 TwoString 類和一個使用一對 String s的構造函數,如清單1所示:
清單2中的代碼獲得構造函數并使用它來創建使用 String s "a" 和 "b" 的 TwoString 類的一個實例:
清單2中的代碼忽略了不同反射方法拋出的多種可能選中的例外類型。例外在 Javadoc API 描述中詳細記錄,因此為了簡明起見,我將在所有程序實例中忽略它們。
盡管我在討論構造函數主題,Java編程語言還定義了一種您可以用來使用 無參數(或缺省)構造函數創建類的一個實例的特殊快捷方式。這種快捷方式嵌入到 Class 定義中,如下:
Object newInstance() -- 使用缺省函數創建新的實例
即使這種方法只允許您使用一種特殊的構造函數,如果這正是您需要的,那么它將提供一種非常方便的快捷方式。當與JavaBeans協作時這項技術尤其有用,JavaBeans需要定義公共、無參數構造函數。
獲得字段信息的 Class 反射調用不同于那些用于接入構造函數的調用,在參數類型數組中使用了字段名:
Field getField(String name) -- 獲得命名的公共字段
Field[] getFields() -- 獲得類的所有公共字段
Field getDeclaredField(String name) -- 獲得類聲明的命名的字段
Field[] getDeclaredFields() -- 獲得類聲明的所有字段
盡管與構造函數調用類似,在字段方面仍存在一個重要的區別:前兩個變量返回可以通過類接入的公共字段的信息 -- 即使它們來自于祖先類。后兩個變量返回類直接聲明的字段的信息 -- 與字段的接入類型無關。
調用返回的 java.lang.reflect.Field 實例定義所有主類型的 getXXX 和 setXXX 方法,以及與對象引用協作的通用 get 和 set 方法。您可以根據實際的字段類型自行選擇一種適當的方法,而 getXXX 方法將自動處理擴展轉換(如使用 getInt 方法來檢索一個字節值)。
清單3顯示使用字段反射方法的一個實例,以方法的格式根據名稱增加對象的 int 字段 :
這種方法開始展示了反射帶來的某些靈活性。與特定的類協作不同, incrementField 使用傳 入的對象的 getClass 方法來查找類信息,然后直接在該類中查找命名的字段。
獲得方法信息的 Class 反射調用與用于構造函數和字段的調用非常類似:
Method getMethod(String name, Class[] params) -- 使用特定的參數類型,獲得命名的公共方法
Method[] getMethods() -- 獲得類的所有公共方法
Method getDeclaredMethod(String name, Class[] params) -- 使用特寫的參數類型,獲得類聲明的命名的方法
Method[] getDeclaredMethods() -- 獲得類聲明的所有方法
與字段調用一樣,前兩個變量返回可以通過類接入的公共方法的信息 -- 即使它們來自于祖先類。后兩個變量返回類聲明的方法的信息,與方法的接入類型無關。
調用返回的 java.lang.reflect.Method 實例定義一種 invoke 方法,您可以用來在正在定義的類的一個實例上調用方法。這種 invoke 方法使用兩個參數,為調用提供類實例和參數值數組。
清單4進一步闡述字段實例,顯示反射正在運行的方法的一個實例。這種方法增加一個定義有 get 和 set 方法的 int JavaBean屬性。例如,如果對象為一個整數 count 值定義了 getCount 和 setCount 方法,您可以在一次調用中向該方法傳遞“count”作為 name 參數,以增加該值。
為了遵循JavaBeans慣例,我把屬性名的首字母改為大寫,然后預先考慮 get 來創建讀方法名, set 來創建寫方法名。JavaBeans讀方法僅返回值,而寫方法使用值作為唯一的參數,因此我規定方法的參數類型以進行匹配。最后,該慣例要求方法為公共,因此我使用查找格式,查找類上可調用的公共方法。
這一實例是第一個我使用反射傳遞主值的實例,因此現在我們來看看它是如何工作的。基本原理很簡單:無論什么時候您需要傳遞主值,只需用相應封裝類的一個實例(在 java.lang 包中定義)來替換該類主值。這可以應用于調用和返回。因此,當我在實例中調用 get 方法時,我預計結果為實際 int 屬性值的 java.lang.Integer 封裝。
數組是Java編程語言中的對象。與所有對象一樣,它們都有類。如果您有一個數組,使用標準 getClass 方法,您可以獲得該數組的類,就象任何其它對象一樣。但是, 不通過現有的實例來獲得類不同于其它類型的對象。即使您有一個數組類,您也不能直接對它進行太多的操作 -- 反射為標準類提供的構造函數接入不能用于數組,而且數組沒有任何可接入的字段,只有基本的 java.lang.Object 方法定義用于數組對象。
數組的特殊處理使用 java.lang.reflect.Array 類提供的靜態方法的集合。該類中的方法使您能夠創建新數組,獲得數組對象的長度,讀和寫數組對象的索引值。
清單5顯示了一種重新調整現有數組大小的有效方法。它使用反射來創建相同類型的新數組,然后在返回新數組之前,在老數組中復制所有數據。
在處理反射時安全性是一個較復雜的問題。反射經常由框架型代碼使用,由于這一點,您可能希望框架能夠全面接入您的代碼,無需考慮常規的接入限制。但是,在其它情況下,不受控制的接入會帶來嚴重的安全性風險,如當代碼在不值得信任的代碼共享的環境中運行時。
由于這些互相矛盾的需求,Java編程語言定義一種多級別方法來處理反射的安全性。基本模式是對反射實施與應用于源代碼接入相同的的限制:
從任意位置到類公共組件的接入
類自身外部無任何到私有組件的接入
受保護和打包(缺省接入)組件的有限接入
不過-至少某些時候,圍繞這些限制有一種簡單的方法。我在前面實例中使用的 Constructor 、 Field 和 Method 類都擴展了一個普通的基本類--  java.lang.reflect.AccessibleObject 類。該類定義一種 setAccessible 方法,使您能夠啟動或關閉對這些類中其中一個類的實例的接入檢測。唯一的問題在于如果使用了安全性管理器,它將檢測正在關閉接入檢測的代碼是否許可了這樣做。如果未許可,安全性管理器拋出一個例外。
清單6展示了一個程序,在 清單 1 TwoString 類的一個實例上使用反射來顯示安全性正在運行:
如果您編譯了這一程序,不使用任何特定參數直接從命令行運行,它將在 field.get(inst) 調用中拋出一個 IllegalAccessException 。如果您未注釋 field.setAccessible(true) 代碼行,那么重新編譯并重新運行該代碼,它將取得成功。最后,如果您在命令行添加了JVM參數 -Djava.security.manager 以實現安全性管理器,它將再次失敗,除非您定義了 ReflectSecurity 類的許可權限。
反射性能
反射是一種強大的工具,但也存在一些不足。一個主要的缺點是對性能有影響。使用反射基本上是一種解釋操作,您可以告訴JVM您希望做什么并且它滿足您的要求。這類操作總是慢于只直接執行相同的操作。為了闡述使用反射的性能成本,我為本文準備了一組基準程序(見 參考資料,完整代碼鏈接)。
清單7是字段接入性能測試的一個摘用,包括基本的測試方法。每種方法測試字段接入的一種形式 -- accessSame 與同一對象的成員字段協作, accessOther 使用可直接接入的另一對象的字段, accessReflection 使用可通過反射接入的另一對象的字段。在每種情況下,方法執行相同的計算 -- 循環中簡單的加/乘順序。
測試程序重復調用每種方法,使用一個大循環數,從而平均多次調用的時間衡量結果。平均值中不包括每種方法第一次調用的時間,因此初始化時間不是結果中的一個因素。在為本文進行的測試中, 每次調用時我使用1000萬的循環數,在1GHz PIIIm系統上運行。三個不同Linux JVM的計時結果如圖1所示。所有測試使用每個JVM的缺省設置。
上表的對數尺度可以顯示所有時間,但減少了差異看得見的影響。在前兩副圖中(Sun JVM),使用反射的執行時間超過使用直接接入的1000倍以上。通過比較,IBM JVM可能稍好一些,但反射方法仍舊需要比其它方法長700倍以上的時間。任何JVM上其它兩種方法之間時間方面無任何顯著差異,但IBM JVM幾乎比Sun JVM快一倍。最有可能的是這種差異反映了Sun Hot Spot JVM的專業優化,它在簡單基準方面表現得很糟糕。
除了字段接入時間測試之外,我還進行了相同的方法調用時間測試。在方法調用中,我試用了與字段接入相同的三種接入變量,并增加了使用無參數方法變量,而不是在方法調用中傳遞和返回一個值。清單8顯示了用于測試調用傳遞和返回值形式的三種方法的代碼。
圖 2顯示了我從方法調用中獲得的計時結果。反射遠慢于直接接入。差異不象字段接入那么大,但是,在不使用參數的情況下,范圍從Sun 1.3.1 JVM的數百倍到IBM JVM的不到30倍。在所有JVM上,使用參數的反射方法調用的測試性能慢于不使用參數的調用。由于傳遞和返回 int 值需要的 java.lang.Integer 封裝,這可能是局部的。由于 Integer s是不可變的,每種方法返回提出了一種新的需求,它將增加大量的開銷。
反射性能是Sun開發1.4 JVM時關注的一個方面,它在反射方法調用結果中顯示。在這類操作的性能方面,Sun 1.4.1 JVM顯示了比1.3.1版本很大的改進,在我的測試中運行速度大約是1.3.1版本的開部。在這類簡單的測試中,IBM 1.4.0 JVM再次獲得了更好的成績,但是只比Sun 1.4.1 JVM快兩到三倍。
我還為創建使用反射的對象編寫了類似的計時測試程序,但這種情況下的差異不象字段和方法調用情況下那么顯著。使用 newInstance() 調用創建一個簡單的 java.lang.Object 實例耗用的時間大約是在Sun 1.3.1 JVM上使用 new Object() 的12倍,是在IBM 1.4.0 JVM的四倍,只是Sun 1.4.1 JVM上的兩部。使用 Array.newInstance(type, size) 創建一個數組耗用的時間是任何測試的JVM上使用 new type[size] 的兩倍,隨著數組大小的增加,差異逐步縮小。
小結
Java語言反射提供一種動態鏈接程序組件的多功能方法。它允許程序創建和控制任何類的對象(根據安全性限制),無需提前硬編碼目標類。這些特性使得反射特別適用于創建以非常普通的方式與對象協作的庫。例如,反射經常在持續存儲對象為數據庫、XML或其它外部格式的框架中使用。
反射有兩個缺點。第一個是性能問題。當用于字段和方法接入時反射要遠慢于直接代碼。性能問題的程度取決于程序中是如何使用反射的。如果它作為程序運行中相對很少涉及的部分,緩慢的性能將不會是一個問題。即使測試中最壞情況下的計時圖顯示的反射操作只耗用幾微秒。僅反射在性能關鍵的應用的核心邏輯中使用時性能問題才變得至關重要。
許多應用更嚴重的一個缺點是使用反射會模糊程序內部實際要發生的事情。程序人員希望在源代碼中看到程序的邏輯,反射等繞過了源代碼的技術會帶來維護問題。反射代碼比相應的直接代碼更復雜,正如性能比較的代碼實例中看到的一樣。解決這些問題的最佳方案是保守地使用反射-- 僅在它可以真正增加靈活性的地方 -- 記錄其在目標類中的使用。
反射是一種強大的工具。它使您能夠創建靈活的代碼,這些代碼可以在運行時裝配,無需在組件之間進行源代表鏈接。但反射的某些方面存在一些疑問。在本文中,我將深入討論為什么您可能 不希望在程序中使用反射,以及您應該這樣做的理由。在了解了權衡性分析之后,您可以自行決定是否利大于弊。
初學者的類
使用反射的啟點總是 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 信息。如果類未被裝入,類裝入器將現在裝入并返回新創建的類實例。
基于類的反射
Class 對象為您提供接入類元數據的反射的所有基本hook。這類元數據包括關于類自身的信息,如包和類的父類,以及該類實施的接口。它還包括該類定義的構造函數、字段和方法的詳細信息。這些最后的項目都是編程中最經常使用的項目, 因此我將在本小節的稍后部分給出一些與它們協作的實例。
對于以下三類組件中的任何一類來說 -- 構造函數、字段和方法 -- java.lang.Class 提供四種獨立的反射調用,以不同的方式來獲得信息。調用都遵循一種標準格式。以下是用于查找構造函數的一組反射調用:
Constructor getConstructor(Class[] params) -- 獲得使用特殊的參數類型的公共構造函數,
Constructor[] getConstructors() -- 獲得類的所有公共構造函數
Constructor getDeclaredConstructor(Class[] params) -- 獲得使用特定參數類型的構造函數(與接入級別無關)
Constructor[] getDeclaredConstructors() -- 獲得類的所有構造函數(與接入級別無關)
每類這些調用都返回一個或多個 java.lang.reflect.Constructor 函數。這種 Constructor 類定義 newInstance 方法,它采用一組對象作為其唯一的參數,然后返回新創建的原始類實例。該組對象是用于構造函數調用的參數值。作為解釋這一工作流程的實例,假設您有一個 TwoString 類和一個使用一對 String s的構造函數,如清單1所示:
public class TwoString { private String m_s1, m_s2; public TwoString(String s1, String s2) { m_s1 = s1; m_s2 = s2; } }
清單2中的代碼獲得構造函數并使用它來創建使用 String s "a" 和 "b" 的 TwoString 類的一個實例:
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);
清單2中的代碼忽略了不同反射方法拋出的多種可能選中的例外類型。例外在 Javadoc API 描述中詳細記錄,因此為了簡明起見,我將在所有程序實例中忽略它們。
盡管我在討論構造函數主題,Java編程語言還定義了一種您可以用來使用 無參數(或缺省)構造函數創建類的一個實例的特殊快捷方式。這種快捷方式嵌入到 Class 定義中,如下:
Object newInstance() -- 使用缺省函數創建新的實例
即使這種方法只允許您使用一種特殊的構造函數,如果這正是您需要的,那么它將提供一種非常方便的快捷方式。當與JavaBeans協作時這項技術尤其有用,JavaBeans需要定義公共、無參數構造函數。
通過反射增加字段
獲得字段信息的 Class 反射調用不同于那些用于接入構造函數的調用,在參數類型數組中使用了字段名:
Field getField(String name) -- 獲得命名的公共字段
Field[] getFields() -- 獲得類的所有公共字段
Field getDeclaredField(String name) -- 獲得類聲明的命名的字段
Field[] getDeclaredFields() -- 獲得類聲明的所有字段
盡管與構造函數調用類似,在字段方面仍存在一個重要的區別:前兩個變量返回可以通過類接入的公共字段的信息 -- 即使它們來自于祖先類。后兩個變量返回類直接聲明的字段的信息 -- 與字段的接入類型無關。
調用返回的 java.lang.reflect.Field 實例定義所有主類型的 getXXX 和 setXXX 方法,以及與對象引用協作的通用 get 和 set 方法。您可以根據實際的字段類型自行選擇一種適當的方法,而 getXXX 方法將自動處理擴展轉換(如使用 getInt 方法來檢索一個字節值)。
清單3顯示使用字段反射方法的一個實例,以方法的格式根據名稱增加對象的 int 字段 :
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) -- 使用特定的參數類型,獲得命名的公共方法
Method[] getMethods() -- 獲得類的所有公共方法
Method getDeclaredMethod(String name, Class[] params) -- 使用特寫的參數類型,獲得類聲明的命名的方法
Method[] getDeclaredMethods() -- 獲得類聲明的所有方法
與字段調用一樣,前兩個變量返回可以通過類接入的公共方法的信息 -- 即使它們來自于祖先類。后兩個變量返回類聲明的方法的信息,與方法的接入類型無關。
調用返回的 java.lang.reflect.Method 實例定義一種 invoke 方法,您可以用來在正在定義的類的一個實例上調用方法。這種 invoke 方法使用兩個參數,為調用提供類實例和參數值數組。
清單4進一步闡述字段實例,顯示反射正在運行的方法的一個實例。這種方法增加一個定義有 get 和 set 方法的 int JavaBean屬性。例如,如果對象為一個整數 count 值定義了 getCount 和 setCount 方法,您可以在一次調用中向該方法傳遞“count”作為 name 參數,以增加該值。
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讀方法僅返回值,而寫方法使用值作為唯一的參數,因此我規定方法的參數類型以進行匹配。最后,該慣例要求方法為公共,因此我使用查找格式,查找類上可調用的公共方法。
這一實例是第一個我使用反射傳遞主值的實例,因此現在我們來看看它是如何工作的。基本原理很簡單:無論什么時候您需要傳遞主值,只需用相應封裝類的一個實例(在 java.lang 包中定義)來替換該類主值。這可以應用于調用和返回。因此,當我在實例中調用 get 方法時,我預計結果為實際 int 屬性值的 java.lang.Integer 封裝。
反射數組
數組是Java編程語言中的對象。與所有對象一樣,它們都有類。如果您有一個數組,使用標準 getClass 方法,您可以獲得該數組的類,就象任何其它對象一樣。但是, 不通過現有的實例來獲得類不同于其它類型的對象。即使您有一個數組類,您也不能直接對它進行太多的操作 -- 反射為標準類提供的構造函數接入不能用于數組,而且數組沒有任何可接入的字段,只有基本的 java.lang.Object 方法定義用于數組對象。
數組的特殊處理使用 java.lang.reflect.Array 類提供的靜態方法的集合。該類中的方法使您能夠創建新數組,獲得數組對象的長度,讀和寫數組對象的索引值。
清單5顯示了一種重新調整現有數組大小的有效方法。它使用反射來創建相同類型的新數組,然后在返回新數組之前,在老數組中復制所有數據。
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編程語言定義一種多級別方法來處理反射的安全性。基本模式是對反射實施與應用于源代碼接入相同的的限制:
從任意位置到類公共組件的接入
類自身外部無任何到私有組件的接入
受保護和打包(缺省接入)組件的有限接入
不過-至少某些時候,圍繞這些限制有一種簡單的方法。我在前面實例中使用的 Constructor 、 Field 和 Method 類都擴展了一個普通的基本類--  java.lang.reflect.AccessibleObject 類。該類定義一種 setAccessible 方法,使您能夠啟動或關閉對這些類中其中一個類的實例的接入檢測。唯一的問題在于如果使用了安全性管理器,它將檢測正在關閉接入檢測的代碼是否許可了這樣做。如果未許可,安全性管理器拋出一個例外。
清單6展示了一個程序,在 清單 1 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您希望做什么并且它滿足您的要求。這類操作總是慢于只直接執行相同的操作。為了闡述使用反射的性能成本,我為本文準備了一組基準程序(見 參考資料,完整代碼鏈接)。
清單7是字段接入性能測試的一個摘用,包括基本的測試方法。每種方法測試字段接入的一種形式 -- accessSame 與同一對象的成員字段協作, accessOther 使用可直接接入的另一對象的字段, 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; } }
測試程序重復調用每種方法,使用一個大循環數,從而平均多次調用的時間衡量結果。平均值中不包括每種方法第一次調用的時間,因此初始化時間不是結果中的一個因素。在為本文進行的測試中, 每次調用時我使用1000萬的循環數,在1GHz PIIIm系統上運行。三個不同Linux JVM的計時結果如圖1所示。所有測試使用每個JVM的缺省設置。

上表的對數尺度可以顯示所有時間,但減少了差異看得見的影響。在前兩副圖中(Sun JVM),使用反射的執行時間超過使用直接接入的1000倍以上。通過比較,IBM JVM可能稍好一些,但反射方法仍舊需要比其它方法長700倍以上的時間。任何JVM上其它兩種方法之間時間方面無任何顯著差異,但IBM JVM幾乎比Sun JVM快一倍。最有可能的是這種差異反映了Sun Hot Spot JVM的專業優化,它在簡單基準方面表現得很糟糕。
除了字段接入時間測試之外,我還進行了相同的方法調用時間測試。在方法調用中,我試用了與字段接入相同的三種接入變量,并增加了使用無參數方法變量,而不是在方法調用中傳遞和返回一個值。清單8顯示了用于測試調用傳遞和返回值形式的三種方法的代碼。
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; } }
圖 2顯示了我從方法調用中獲得的計時結果。反射遠慢于直接接入。差異不象字段接入那么大,但是,在不使用參數的情況下,范圍從Sun 1.3.1 JVM的數百倍到IBM JVM的不到30倍。在所有JVM上,使用參數的反射方法調用的測試性能慢于不使用參數的調用。由于傳遞和返回 int 值需要的 java.lang.Integer 封裝,這可能是局部的。由于 Integer s是不可變的,每種方法返回提出了一種新的需求,它將增加大量的開銷。

反射性能是Sun開發1.4 JVM時關注的一個方面,它在反射方法調用結果中顯示。在這類操作的性能方面,Sun 1.4.1 JVM顯示了比1.3.1版本很大的改進,在我的測試中運行速度大約是1.3.1版本的開部。在這類簡單的測試中,IBM 1.4.0 JVM再次獲得了更好的成績,但是只比Sun 1.4.1 JVM快兩到三倍。
我還為創建使用反射的對象編寫了類似的計時測試程序,但這種情況下的差異不象字段和方法調用情況下那么顯著。使用 newInstance() 調用創建一個簡單的 java.lang.Object 實例耗用的時間大約是在Sun 1.3.1 JVM上使用 new Object() 的12倍,是在IBM 1.4.0 JVM的四倍,只是Sun 1.4.1 JVM上的兩部。使用 Array.newInstance(type, size) 創建一個數組耗用的時間是任何測試的JVM上使用 new type[size] 的兩倍,隨著數組大小的增加,差異逐步縮小。
小結
Java語言反射提供一種動態鏈接程序組件的多功能方法。它允許程序創建和控制任何類的對象(根據安全性限制),無需提前硬編碼目標類。這些特性使得反射特別適用于創建以非常普通的方式與對象協作的庫。例如,反射經常在持續存儲對象為數據庫、XML或其它外部格式的框架中使用。
反射有兩個缺點。第一個是性能問題。當用于字段和方法接入時反射要遠慢于直接代碼。性能問題的程度取決于程序中是如何使用反射的。如果它作為程序運行中相對很少涉及的部分,緩慢的性能將不會是一個問題。即使測試中最壞情況下的計時圖顯示的反射操作只耗用幾微秒。僅反射在性能關鍵的應用的核心邏輯中使用時性能問題才變得至關重要。
許多應用更嚴重的一個缺點是使用反射會模糊程序內部實際要發生的事情。程序人員希望在源代碼中看到程序的邏輯,反射等繞過了源代碼的技術會帶來維護問題。反射代碼比相應的直接代碼更復雜,正如性能比較的代碼實例中看到的一樣。解決這些問題的最佳方案是保守地使用反射-- 僅在它可以真正增加靈活性的地方 -- 記錄其在目標類中的使用。
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
