java使用反射建立並操作物件的方法
Class 物件可以獲得該類裡的方法(由 Method 物件表示)、構造器(由 Constructor 物件表示)、成員變數(由 Field 物件表示),這三個類都位於 java.lang.reflect 包下,並實現了 java.lang.reflect.Member 介面。程式可以通過物件來執行對應的方法,通過 Constructor 物件來呼叫對應的構造器建立例項,能通過 Field 物件直接訪問並修改物件的成員變數值。
建立物件
通過反射來生成物件需要先使用 Class 物件獲取指定的 Constructor 物件,再呼叫 Constructor 物件的 newInstance() 方法來建立該 Class 物件對應類的例項。通過這種方式可以選擇使用指定的構造器來建立例項。
在很多 Java EE 框架中都需要根據配置檔案資訊來建立 Java 物件,從配置檔案讀取的只是某個類的字串類名,程式需要根據該字串來建立對應的例項,就必須使用反射。
下面程式就實現了一個簡單的物件池,該物件池會根據配置檔案讀取 key-value 對,然後建立這些物件,並將這些物件放入一個 HashMap 中。
public class ObjectPoolFactory { // 定義一個物件池,前面是物件名,後面是實際物件 private Map<String,Object> objectPool = new HashMap<>(); // 定義一個建立物件的方法 // 該方法只要傳入一個字串類名,程式可以根據該類名生成Java物件 private Object createObject(String clazzName) throws Exception,IllegalAccessException,ClassNotFoundException { // 根據字串來獲取對應的Class物件 Class<?> clazz = Class.forName(clazzName); // 使用clazz對應類的預設構造器建立例項 return clazz.getConstructor().newInstance(); } // 該方法根據指定檔案來初始化物件池 // 它會根據配置檔案來建立物件 public void initPool(String fileName) throws InstantiationException,ClassNotFoundException { try (FileInputStream fis = new FileInputStream(fileName)) { Properties props = new Properties(); props.load(fis); for (String name : props.stringPropertyNames()) { // 每取出一對key-value對,就根據value建立一個物件 // 呼叫createObject()建立物件,並將物件新增到物件池中 objectPool.put(name,createObject(props.getProperty(name))); } } catch (Exception ex) { System.out.println("讀取" + fileName + "異常"); } } public Object getObject(String name) { // 從objectPool中取出指定name對應的物件 return objectPool.get(name); } public static void main(String[] args) throws Exception { ObjectPoolFactory pf = new ObjectPoolFactory(); pf.initPool("obj.txt"); System.out.println(pf.getObject("a")); // ① System.out.println(pf.getObject("b")); // ② } }
上面程式中 createObject() 方法裡的兩行粗體字程式碼就是根據字串來建立 Java 物件的關鍵程式碼,程式呼叫 Class 物件的 newInstance() 方法即可建立一個 Java 物件。程式中的 initPool() 方法會讀取屬性檔案,對屬性檔案中每個 key-value 對建立一個 Java 物件,其中 value 是該 Java 物件的實現類,而 key 是該 Java 物件放入物件池中的名字。為該程式提供如下屬性配置檔案。
a=java.util.Date
b=javax.swing.JFrame
編譯、執行上面的 ObjectPoolFactory 程式,執行到 main 方法中的①號程式碼處,將看到輸出系統當前時間——這表明物件池中已經有了一個名為a的物件,該物件是一個 java.util.Date 物件。執行到②號程式碼處,將看到輸出一個 JFrame 物件。
提示:這種使用配置檔案來配置物件,然後由程式根據配置檔案來建立物件的方式非常有用,大名鼎鼎的 Spring 框架就採用這種方式大大簡化了 Java EE 應用的開發。當然,Spring 採用的是 XML 配置檔案——畢竟屬性檔案能配置的資訊太有限了,而 XML 配置檔案能配置的資訊就豐富多。
如果不想利用預設構造器來建立 Java 物件,而想利用指定的構造器來建立 Java 物件,則需要利用 Constructor 物件,每個 Constructor 對應一個構造器。為了利用指定的構造器來建立 Java 物件,需要如下三個步驟。
- 獲取該類的 Class 物件。
- 利用 Class 物件的 getConstructor() 方法來獲取指定的構造器。
- 呼叫 Constructor 的 newInstance() 方法來建立 Java 物件。
下面程式利用反射來建立一個 JFrame 物件,而且使用指定的構造器。
public class CreateJFrame { public static void main(String[] args) throws Exception { // 獲取JFrame對應的Class物件 Class<?> jframeClazz = Class.forName("javax.swing.JFrame"); // 獲取JFrame中帶一個字串引數的構造器 Constructor ctor = jframeClazz.getConstructor(String.class); // 呼叫Constructor的newInstance方法建立物件 Object obj = ctor.newInstance("測試視窗"); // 輸出JFrame物件 System.out.println(obj); } }
上面程式中第一行粗休字程式碼用於獲取 JFrame 類的指定構造器,前面已經提到:如果要唯一地確定某類中的構造器,只要指定構造器的形參列表即可。第一行粗體字程式碼獲取構造器時傳入了一個 String 型別,即表明想獲取只有一個字串引數的構造器。
程式中第二行粗體字程式碼使用指定構造器的 newInstance() 方法來建立一個 Java 物件,當呼叫 Constructor 物件的 newInstance() 方法時通常需要傳入引數,因為呼叫 Constructor 的 newInstance() 方法實際上等於呼叫它對應的構造器,傳給 newInstance() 方法的引數將作為對應構造器的引數。
對於上面的 CreateFrame.java 中已知 java.swing.JFrame 類的情形,通常沒有必要使用反射來建立該物件,畢竟通過反射建立物件時效能要稍低一些。實際上,只有當程式需要動態建立某個類的物件時才會考慮使用反射,通常在開發通用性比較廣的框架、基礎平臺時可能會大量使用反射。
呼叫方法
當獲得某個類對應的 Class 物件後,就可以通過該 Class 物件的 getMethods() 方法或者 getMethod()方法來獲取全部方法或指定方法——這兩個方法的返回值是 Method 陣列,或者 Method 物件。
每個 Method 物件對應一個方法,獲得 Method 物件後,程式就可通過該 Method 來呼叫它對應的方法。在 Method 裡包含一個 Invoke() 方法,該方法的簽名如下。
- Object invoke(Object obj,Object...args):該方法中的 obj 是執行該方法的主調,後面的 args 是執行該方法時傳入該方法的實參。
下面程式對前面的物件池工廠進行加強,允許在配置檔案中增加配置物件的成員變數的值,物件池工廠會讀取為該物件配置的成員變數值,並利用該物件對應的 setter 方法設定成員變數的值。
public class ExtendedObjectPoolFactory { // 定義一個物件池,前面是物件名,後面是實際物件 private Map<String,Object> objectPool = new HashMap<>(); private Properties config = new Properties(); // 從指定屬性檔案中初始化Properties物件 public void init(String fileName) { try (FileInputStream fis = new FileInputStream(fileName)) { config.load(fis); } catch (IOException ex) { System.out.println("讀取" + fileName + "異常"); } } // 定義一個建立物件的方法 // 該方法只要傳入一個字串類名,程式可以根據該類名生成Java物件 private Object createObject(String clazzName) throws Exception { // 根據字串來獲取對應的Class物件 Class<?> clazz = Class.forName(clazzName); // 使用clazz對應類的預設構造器建立例項 return clazz.getConstructor().newInstance(); } // 該方法根據指定檔案來初始化物件池 // 它會根據配置檔案來建立物件 public void initPool() throws Exception { for (String name : config.stringPropertyNames()) { // 每取出一個key-value對,如果key中不包含百分號(%) // 這就表明是根據value來建立一個物件 // 呼叫createObject建立物件,並將物件新增到物件池中 if (!name.contains("%")) { objectPool.put(name,createObject(config.getProperty(name))); } } } // 該方法將會根據屬性檔案來呼叫指定物件的setter方法 public void initProperty() throws InvocationTargetException,NoSuchMethodException { for (String name : config.stringPropertyNames()) { // 每取出一對key-value對,如果key中包含百分號(%) // 即可認為該key用於控制呼叫物件的setter方法設定值 // %前半為物件名字,後半控制setter方法名 if (name.contains("%")) { // 將配置檔案中的key按%分割 String[] objAndProp = name.split("%"); // 取出呼叫setter方法的引數值 Object target = getObject(objAndProp[0]); // 獲取setter方法名:set + "首字母大寫" + 剩下部分 String mtdName = "set" + objAndProp[1].substring(0,1).toUpperCase() + objAndProp[1].substring(1); // 通過target的getClass()獲取它的實現類所對應的Class物件 Class<?> targetClass = target.getClass(); // 獲取希望呼叫的setter方法 Method mtd = targetClass.getMethod(mtdName,String.class); // 通過Method的invoke方法執行setter方法 // 將config.getProperty(name)的值作為呼叫setter方法的引數 mtd.invoke(target,config.getProperty(name)); } } } public Object getObject(String name) { // 從objectPool中取出指定name對應的物件 return objectPool.get(name); } public static void main(String[] args) throws Exception { ExtendedObjectPoolFactory epf = new ExtendedObjectPoolFactory(); epf.init("extObj.txt"); epf.initPool(); epf.initProperty(); System.out.println(epf.getObject("a")); } }
上面程式中 initProperty() 方法裡的第一行粗體字程式碼獲取目標類中包含一個 String 引數的 setter 方法,第二行粗體字程式碼通過呼叫 Method 的 invoke() 方法來執行該 setter 方法,該方法執行完成後,就相當於執行了目標物件的 setter 方法。為上面程式提供如下配置檔案。
a=javax.swing.JFrame
b=javax.swing.JLabel
#set the title of a
a%title=Test Title
上面配置檔案中的 a%title 行表明希望呼叫a物件的 setTitle() 方法,呼叫該方法的引數值為 Test Title。編譯、執行上面的 ExtendedObjectPoolFactory.java 程式,可以看到輸出一個 JFrame 視窗,該視窗的標題為 Test Title。
提示:Spring 框架就是通過這種方式將成員變數值以及依賴物件等都放在配置檔案中進行管理的,從而實現了較好的解耦。這也是 Spring 框架的 IOC 的祕密。
當通過 Method 的 invoke() 方法來呼叫對應的方法時,Java 會要求程式必須有呼叫該方法的許可權。如果程式確實需要呼叫某個物件的 private 方法,則可以先呼叫 Method 物件的如下方法。
- setAccessible(boolean flag):將 Method 物件的 accessible 設定為指定的布林值。值為true,指示該 Method 在使用時應該取消 Java 語言的訪問許可權檢查:值為false,則指示該 Method 在使用時要實施 Java 語言的訪問許可權檢查。
注意:實際上,setAccessible() 方法並不屬於 Method,而是屬於它的父類 AccessibleObject。因此 Method、Constructor、Field 都可呼叫該方法,從而實現通過反射來呼叫 private 方法、private 構造器和成員變數,下一節將會讓讀者看到這種示例。也就是說,它們可以通過呼叫該方法來取消訪問許可權檢查,通過反射即可訪問 private 成員。
訪問成員變數值
通過 Class 物件的 getFields() 或 getField() 方法可以獲取該類所包括的全部成員變數或指定成員變數。Field 提供瞭如下兩組方法來讀取或設定成員變數值。
- getXxx(Object obj):獲取 obj 物件的該成員變數的值。此處的 Xxx 對應8種基本型別,如果該成員變數的型別是引用型別,則取消 get 後面的Xxx。
- setXxx(Object obj,Xxx val):將 obj 物件的該成員變數設定成值。此處的 Xxx 對應8種基本型別,如果該成員變數的型別是引用型別,則取消 set 後面的Xxx。
使用這兩個方法可以隨意地訪問指定物件的所有成員變數,包括 private 修飾的成員變數。
class Person { private String name; private int age; public String toString() { return "Person[name:" + name + ",age:" + age + " ]"; } } public class FieldTest { public static void main(String[] args) throws Exception { // 建立一個Person物件 Person p = new Person(); // 獲取Person類對應的Class物件 Class<Person> personClazz = Person.class; // 獲取Person的名為name的成員變數 // 使用getDeclaredField()方法表明可獲取各種訪問控制符的成員變數 Field nameField = personClazz.getDeclaredField("name"); // 設定通過反射訪問該成員變數時取消訪問許可權檢查 nameField.setAccessible(true); // 呼叫set()方法為p物件的name成員變數設定值 nameField.set(p,"Yeeku.H.Lee"); // 獲取Person類名為age的成員變數 Field ageField = personClazz.getDeclaredField("age"); // 設定通過反射訪問該成員變數時取消訪問許可權檢查 ageField.setAccessible(true); // 呼叫setInt()方法為p物件的age成員變數設定值 ageField.setInt(p,30); System.out.println(p); } }
上面程式中先定義了一個 Person 類,該類裡包含兩個 private 成員變數:name 和 age,在通常情況下,這兩個成員變數只能在 Person 類裡訪問。但本程式 FieldTest 的 main() 方法中6行粗體字程式碼通過反射修改了 Person 物件的 name、age 兩個成員變數的值。
第一行粗體字程式碼使用 getDeclaredField() 方法獲取了名為 name 的成員變數,注意此處不是使用 getField()方法,因為 getField() 方法只能獲取 public 訪問控制的成員變數,而 getDeclaredField() 方法則可以獲取所有的成員變數;第二行粗體字程式碼則通過反射訪問該成員變數時不受訪問許可權的控制;第三行粗體字程式碼修改了 Person 物件的 name 成員變數的值。修改 Person 物件的 age 成員變數的值的方式與此完全相同。
編譯、執行上面程式,會看到如下輸出:
Person[name:Yeeku.H.Lee,age:30 ]
運算元組
在 java.lang.reflect 包下還提供了一個 Array 類,Array 物件可以代表所有的陣列。程式可以通過使用 Array 來動態地建立陣列,運算元組元素等。
Array 提供瞭如下幾類方法。
- static Object newInstance(Class<?> componentType,int...length):建立一個具有指定的元素型別、指定維度的新陣列。
- static xxx getXxx(Object array,int index):返回 array 陣列中第 index 個元素。其中是各種基本資料型別,如果陣列元素是引用型別,則該方法變為 get(Object array,int index)。
- static void setXxx(Object array,int index,xxx val):將 array 陣列中第 index 個元素的值設為 val。其中 xxx 是各種基本資料型別,如果陣列元素是引用型別,則該方法變成 set(Object array,Object val)。
下面程式示範瞭如何使用 Array 來生成陣列,為指定陣列元素賦值,並獲取指定陣列元素的方式。
public class ArrayTest1 { public static void main(String args[]) { try { // 建立一個元素型別為String ,長度為10的陣列 Object arr = Array.newInstance(String.class,10); // 依次為arr陣列中index為5、6的元素賦值 Array.set(arr,5,"瘋狂Java講義"); Array.set(arr,6,"輕量級Java EE企業應用實戰"); // 依次取出arr陣列中index為5、6的元素的值 Object book1 = Array.get(arr,5); Object book2 = Array.get(arr,6); // 輸出arr陣列中index為5、6的元素 System.out.println(book1); System.out.println(book2); } catch (Throwable e) { System.err.println(e); } } }
上面程式中三行粗體字程式碼分別是通過 Array 建立陣列,為陣列元素設定值,訪問陣列元素的值的示例程式碼,程式通過使用 Array 就可以動態地建立並運算元組。
下面程式比上面程式稍微複雜一點,下面程式使用 Array 類建立了一個三維陣列。
public class ArrayTest2 { public static void main(String args[]) { /* * 建立一個三維陣列。 根據前面介紹陣列時講的:三維陣列也是一維陣列, 是陣列元素是二維陣列的一維陣列, * 因此可以認為arr是長度為3的一維陣列 */ Object arr = Array.newInstance(String.class,3,4,10); // 獲取arr陣列中index為2的元素,該元素應該是二維陣列 Object arrObj = Array.get(arr,2); // 使用Array為二維陣列的陣列元素賦值。二維陣列的陣列元素是一維陣列, // 所以傳入Array的set()方法的第三個引數是一維陣列。 Array.set(arrObj,2,new String[] { "瘋狂Java講義","輕量級Java EE企業應用實戰" }); // 獲取arrObj陣列中index為3的元素,該元素應該是一維陣列。 Object anArr = Array.get(arrObj,3); Array.set(anArr,8,"瘋狂Android講義"); // 將arr強制型別轉換為三維陣列 String[][][] cast = (String[][][]) arr; // 獲取cast三維陣列中指定元素的值 System.out.println(cast[2][3][8]); System.out.println(cast[2][2][0]); System.out.println(cast[2][2][1]); } }
上面程式的第一行粗體字程式碼使用 Array 建立了一個三維陣列,程式中較難理解的地方是第二段粗體字程式碼部分,使用 Array 為 arrObj 的指定元素賦值,相當於為二維陣列的元素賦值。由於二維陣列的元素是一維陣列,所以程式傳入的引數是一個一維陣列物件。
執行上面程式,將看到 cast[2][3][8]、cast[2][2][0]、cast[2][2][1] 元素都有值,這些值就是剛才程式通過反射傳入的陣列元素值。
以上就是java使用反射建立並操作物件的方法的詳細內容,更多關於JAVA 反射建立並操作物件的資料請關注我們其它相關文章!