Java 反射 Method的invoke回撥呼叫任意方法
invoke回撥流程示例
0.由Class物件動態構造對應型別物件
1.Class物件的getMethod方法,由方法名和形參構造Method物件
2.Method物件的invoke方法來委託動態構造的對應型別物件,使其執行對應形參的add方法,這是回撥函式(方法)的功能
“回撥函式就是一個通過函式指標呼叫的函式。如果你把函式的指標(地址)作為引數傳遞給另一個函式,當這個指標被用來呼叫其所指向的函式時,我們就說這是回撥函式。回撥函式不是由該函式的實現方直接呼叫,而是在特定的事件或條件發生時由另外的一方呼叫的,用於對該事件或條件進行響應。”
//動態構造InvokeTest類的例項 Class<?> classType = InvokeTest.class; Object invokeTest = classType.newInstance(); //動態構造InvokeTest類的add(int num1, int num2)方法,標記為addMethod的Method物件 Method addMethod = classType.getMethod("add", new Class[]{int.class, int.class}); //動態構造的Method物件invoke委託動態構造的InvokeTest物件,執行對應形參的add方法 Object result = addMethod.invoke(invokeTest, new Object[]{1, 2}); //測試輸出 System.out.println((Integer)result);
public Method getMethod(String name, Class<?>... parameterTypes) throws NoSuchMethodException, SecurityException返回一個 Method 物件,它反映此 Class 物件所表示的類或介面的指定公共成員方法。name 引數是一個 String,用於指定所需方法的簡稱。parameterTypes 引數是按宣告順序標識該方法形參型別的 Class 物件的一個數組。如果 parameterTypes 為 null,則按空陣列處理。 如果 name 是 "<init>;" 或 "<clinit>",則將引發 NoSuchMethodException。否則,要反映的方法由下面的演算法確定(設 C 為此物件所表示的類): 在 C 中搜索任一匹配的方法。如果找不到匹配的方法,則將在 C 的超類上遞迴呼叫第 1 步演算法。 如果在第 1 步中沒有找到任何方法,則在 C 的超介面中搜索匹配的方法。如果找到了這樣的方法,則反映該方法。 在 C 類中查詢匹配的方法:如果 C 正好聲明瞭一個具有指定名稱的公共方法並且恰恰有相同的形參型別,則它就是反映的方法。如果在 C 中找到了多個這樣的方法,並且其中有一個方法的返回型別比其他方法的返回型別都特殊,則反映該方法;否則將從中任選一個方法。 注意,類中可以有多個匹配方法,因為儘管 Java 語言禁止類宣告帶有相同簽名但不同返回型別的多個方法,但 Java 虛擬機器並不禁止。這增加了虛擬機器的靈活性,可以用來實現各種語言特性。例如,可以使用橋方法 (brige method)實現協變返回;橋方法以及將被重寫的方法將具有相同的簽名,不同的返回型別。 請參閱Java 語言規範 第 8.2 和 8.4 節。 引數: name - 方法名 parameterTypes - 引數列表 返回: 與指定的 name 和 parameterTypes 匹配的 Method 物件 丟擲: NoSuchMethodException - 如果找不到匹配的方法,或者方法名為 "<init>" 或 "<clinit>" NullPointerException - 如果 name 為 null SecurityException - 如果存在安全管理器 s,並滿足下列任一條件: 呼叫 s.checkMemberAccess(this, Member.PUBLIC) 拒絕訪問方法 呼叫者的類載入器不同於也不是當前類的類載入器的一個祖先,並且對 s.checkPackageAccess() 的呼叫拒絕訪問該類的包
public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException對帶有指定引數的指定物件呼叫由此 Method 物件表示的底層方法。個別引數被自動解包,以便與基本形參相匹配,基本引數和引用引數都隨需服從方法呼叫轉換。 如果底層方法是靜態的,那麼可以忽略指定的 obj 引數。該引數可以為 null。 如果底層方法所需的形引數為 0,則所提供的 args 陣列長度可以為 0 或 null。 如果底層方法是例項方法,則使用動態方法查詢來呼叫它,這一點記錄在 Java Language Specification, Second Edition 的第 15.12.4.4 節中;在發生基於目標物件的執行時型別的重寫時更應該這樣做。 如果底層方法是靜態的,並且尚未初始化宣告此方法的類,則會將其初始化。 如果方法正常完成,則將該方法返回的值返回給呼叫者;如果該值為基本型別,則首先適當地將其包裝在物件中。但是,如果該值的型別為一組基本型別,則陣列元素不 被包裝在物件中;換句話說,將返回基本型別的陣列。如果底層方法返回型別為 void,則該呼叫返回 null。 引數: obj - 從中呼叫底層方法的物件 args - 用於方法呼叫的引數 返回: 使用引數 args 在 obj 上指派該物件所表示方法的結果 丟擲: IllegalAccessException - 如果此 Method 物件強制執行 Java 語言訪問控制,並且底層方法是不可訪問的。 IllegalArgumentException - 如果該方法是例項方法,且指定物件引數不是宣告底層方法的類或介面(或其中的子類或實現程式)的例項;如果實參和形參的數量不相同;如果基本引數的解包轉換失敗;如果在解包後,無法通過方法呼叫轉換將引數值轉換為相應的形參型別。 InvocationTargetException - 如果底層方法丟擲異常。 NullPointerException - 如果指定物件為 null,且該方法是一個例項方法。 ExceptionInInitializerError - 如果由此方法引起的初始化失敗。
用反射實現獲得執行時物件的拷貝的方法(Field類)
1 import java.lang.class; 2 import java.lang.reflect; 3 4 public class ReflectTester 5 { 6 public Object copy(Object object) throws Exception 7 { 8 Class<?> classType = object.getClass(); 9 Object objectCopy = classType.getConstructor(new Class[]{}).newInstance(new Object[]{}); 10 //獲得物件的所有成員變數 11 Field[] fields = classType.getDeclaredFields(); 12 for(Field field : fields) 13 { 14 String name = field.getName(); 15 //將屬性的首字母轉換為大寫 16 String firstLetter = name.substring(0,1).toUpperCase(); 17 //substring(1)表示從index=1開始到最後的子字串 18 String getMethodName = "get" + firstLetter + name.substring(1); 19 String setMethodName = "set" + firstLetter + name.substring(1); 20 Method getMethod = classType.getMethod(getMethodName, new Class[] {}); 21 Method setMethod = classType.setMethod(getMethodName, new Class[] { field.getType() }); 22 Object value = getMethod.invoke(object, new Object[] {}); 23 setMethod.invoke(objectCopy, new Object[] { value }); 24 } 25 return objectCopy; 26 } 27 public static void main(String[] args) 28 { 29 Customer customer = new Customer("Tom", 20); 30 customer.setId(1L); 31 32 ReflectTester test = new ReflectTester(); 33 Customer customer2 = (Customer) test.copy(customer); 34 System.out.println(customer2.getId() + "," + customer2.getName() + "," + customer2.getAge()); 35 } 36 } 37 38 Class Customer 39 { 40 private float ID; 41 private String name; 42 private int age; 43 public Customer() 44 { 45 this.ID = 1L; 46 this.name = A; 47 this.age = 1; 48 } 49 public Customer(String name, int age) 50 { 51 this(); 52 this.name = name; 53 this.age = age; 54 } 55 public void setID(float ID) 56 { 57 this.ID = ID; 58 } 59 }
0.利用Class物件得到對應型別物件
1.利用Class物件的getDeclaredFields方法,得Field[]陣列
2.遍歷Field[]陣列,構造每一個屬性名的字串,使首字母大寫,再連線get字串,組成setter和getter的命名
3.遍歷的同時,getMethod,引數便是要構建的方法名和形參型別,得到各屬性的setter和getter方法的Method物件
4.同時,得到方法後,呼叫各Method物件的invoke委託對應型別物件執行方法(有實參傳入,或返回值)
Method的invoke方法能呼叫任意方法
Method類中的invoke方法,允許呼叫包裝在Method物件中的方法
Object invoke(Object obj, Object... args) //對應隱式引數和顯式引數
public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException對帶有指定引數的指定物件呼叫由此 Method 物件表示的底層方法。個別引數被自動解包,以便與基本形參相匹配,基本引數和引用引數都隨需服從方法呼叫轉換。 如果底層方法是靜態的,那麼可以忽略指定的 obj 引數。該引數可以為 null。 如果底層方法所需的形引數為 0,則所提供的 args 陣列長度可以為 0 或 null。 如果底層方法是例項方法,則使用動態方法查詢來呼叫它,這一點記錄在 Java Language Specification, Second Edition 的第 15.12.4.4 節中;在發生基於目標物件的執行時型別的重寫時更應該這樣做。 如果底層方法是靜態的,並且尚未初始化宣告此方法的類,則會將其初始化。 如果方法正常完成,則將該方法返回的值返回給呼叫者;如果該值為基本型別,則首先適當地將其包裝在物件中。但是,如果該值的型別為一組基本型別,則陣列元素不 被包裝在物件中;換句話說,將返回基本型別的陣列。如果底層方法返回型別為 void,則該呼叫返回 null。 引數: obj - 從中呼叫底層方法的物件 args - 用於方法呼叫的引數 返回: 使用引數 args 在 obj 上指派該物件所表示方法的結果 丟擲: IllegalAccessException - 如果此 Method 物件強制執行 Java 語言訪問控制,並且底層方法是不可訪問的。 IllegalArgumentException - 如果該方法是例項方法,且指定物件引數不是宣告底層方法的類或介面(或其中的子類或實現程式)的例項;如果實參和形參的數量不相同;如果基本引數的解包轉換失敗;如果在解包後,無法通過方法呼叫轉換將引數值轉換為相應的形參型別。 InvocationTargetException - 如果底層方法丟擲異常。 NullPointerException - 如果指定物件為 null,且該方法是一個例項方法。 ExceptionInInitializerError - 如果由此方法引起的初始化失敗。
0.如果底層方法是靜態的,那麼可以忽略指定的 obj
引數。該引數可以為 null;如果底層方法所需的形引數為 0,則所提供的 args
陣列長度可以為 0 或 null
1.第一個引數是隱式引數(比如this,傳進你要委託的物件);其餘的可變引數提供了顯式引數(Java SE 5.0以前沒有可變引數,必須傳遞物件陣列或者null)
對於靜態方法(屬於類),第一個引數設定為null,作為隱式引數來傳遞
2.如果有返回型別,invoke方法將返回一個名義上的Object型別,實際型別由方法內部決定,所以還要進行強制型別轉換;
而因此如果返回型別是基本型別,為了統一返回型別,將會返回其包裝器型別:
double s = (Double)m2.invoke(harry) //省略顯式引數,因為沒有形參,不需要實參傳入
"個別引數被自動解包,以便與基本形參相匹配,基本引數和引用引數都隨需服從方法呼叫轉換。" --JDK API 1.6 中文版
invoke的缺點
1.invoke的引數和返回值必需時Object型別的,這意味著必須進行多次的型別轉換(特別是基本資料型別),而這將導致編譯器錯過檢查程式碼的機會,有型別安全的風險,只有到了測試階段才會發現這些錯誤,此時找到並改正他們將會更加困難
2.使用反射獲得方法指標的程式碼要比僅僅直接呼叫方法明顯慢一些
3.因此僅在必要時才使用Method物件,而最好使用介面和內部類,不建議Java開發者使用Method物件的回撥功能,使用介面進行回撥不僅會使程式碼的執行速度更快,還更易於維護