【Java技術專題】「攻破技術盲區」帶你攻破你很可能存在的Java技術盲點之動態性技術原理指南(反射技術專題)

@

帶你攻破你很可能存在的Java技術盲點之動態性技術原理指南

本系列技術專題的相關技術指南主要有以下三個方面:
在這裏插入圖片描述

編程語言的類型

學習一門新的動態類型語言可能需要花費較長的時間,使得已經熟悉Java的開發人員更希望繼續使用Java來解決問題。然而,Java本身也支持動態性,在一些需要靈活性的場合可以發揮作用。反射API就是Java中的一個例子,它能夠在運行時通過方法名稱查找並調用方法。Java語言也在不斷更新版本,提高對動態性和靈活性的支持。

整體的編程語言分爲三大類:靜態類型語言和動態類型語言、半靜態半動態類型語言。
在這裏插入圖片描述

靜態類型語言

Java語言是一種靜態類型的編程語言,即要在編譯時進行類型檢查。在Java中,每個變量的類型需要在聲明時顯式指定;所有變量、方法的參數和返回值的類型必須在程序運行之前就已經確定。這種靜態類型特性使得編譯器能夠在編譯時進行大量的類型檢查,從而發現代碼中明顯的類型錯誤。然而,這也意味着代碼中包含了大量不必要的類型聲明,使代碼顯得過於冗長且不夠靈活。相對應的,動態類型語言(如JavaScript和Ruby等)的類型檢查則是在運行時進行的。在這類語言中,源代碼中的變量類型可以在運行時動態確定。

動態類型語言

相比於靜態類型語言,動態類型語言(如JavaScript和Ruby等)的類型檢查是在運行時進行的。在這類語言中,源代碼中不需要顯式地聲明類型,因此,使用動態類型語言編寫的代碼更加簡潔。近年來,動態類型語言的流行也反映了語言中動態性的重要性。適當的動態性對於提高開發效率非常有幫助,因爲它可以減少開發人員需要編寫的代碼量。

技術核心方向

雖然Java是一種靜態類型語言,但是它也提供了使代碼更具靈活性的動態性特性。這些特性包括腳本語言支持API、反射API、動態代理和JSR292中引入的動態語言支持。開發人員可以選擇不同的方式來提高代碼的靈活性。例如,可以使用腳本語言支持API將腳本語言集成到Java程序中,使用反射API在運行時動態調用方法,使用動態代理攔截接口方法調用,或使用JSR292中的方法句柄來實現更多的功能。方法句柄支持多種變換操作,並能滿足不同場合的需求。在這裏插入圖片描述

反射API

反射API是Java語言提供的動態性支持,它允許程序在運行時獲取Java類的內部結構,如構造方法、域和方法等,並與它們進行交互。反射API也能實現許多動態語言常用的實用功能。按照面向對象的思路,應該通過方法來改變對象的狀態,而不是直接修改屬性的值。Java類中的屬性設置和獲取方法名通常遵循JavaBeans規範,以setXxx和getXxx命名。因此,可以編寫一個工具類,用於設置和獲取任何符合JavaBeans規範的對象的屬性。

可以使用Java的反射API實現與JavaScript語言的實現類似的功能,代碼量上並不太有差別。實現思路是先從對象的類中查找方法,再調用該方法並傳入參數。這個靜態方法可以被作爲一個實用工具方法在程序中使用。

public class ReflectSetter
   public static void invokeSetter(Object obj,String field,Object value) throws NoSuchMethodException,InvocationTargetException,IllegalAccessException{
     String methodName "set"+field.substring(0,1).toUppercase() + field.substring(1);
     class<?>clazz obj.getclass();
     Method method clazz.getMethod (methodName,value.getclass ())
     method.invoke (obj,value);
 }
}

從上述示例可以看出,反射API可以實現Java語言的靈活使用。實際上,反射API定義了提供者和使用者之間的鬆散契約,這種契約可以在方法調用時只需要建立在名稱和參數類型上,而不需要在代碼中首先聲明變量。這種方式提供了更大的靈活性和動態性,但也需要開發者自己保證調用的合法性。如果方法調用不合法,相關的異常會在運行時拋出。

反射案例介紹

反射API常用於方法名或屬性名按照特定規則變化的情況:

  • 在Servlet中,利用反射API可以遍歷HTTP請求中的所有參數,然後用invokeSetter方法填充領域對象的屬性值。
  • 在數據庫操作中,也通過反射API實現從查詢結果集中創建並填充領域對象的場景。這些對應關係都可以通過反射API來建立。

反射功能操作

反射API雖然能爲Java程序帶來靈活性,但其實現機制也會帶來性能代價。通過反射調用方法一般比直接在源代碼中編寫的方式慢一到兩個數量級。雖然隨着Java虛擬機的改進,反射API的性能得到了提升,但在一些對性能要求高的應用中,需要慎用反射API。
在這裏插入圖片描述

獲取構造器

可以通過反射API獲取Java類中的構造方法,從而在運行時動態地創建Java對象。具體步驟如下:

  1. 獲取Class類的對象,可以使用Class.forName方法或者類的.class屬性。

  2. 通過Class類的getConstructors方法獲取所有的公開構造方法的列表,或者使用getConstructor方法根據參數類型獲取公開的構造方法。如果需要獲取類中真正聲明的構造方法,可以使用getDeclaredConstructors和getDeclaredConstructor方法。

  3. 得到表示構造方法的java.lang.reflect.Constructor對象之後,可以通過其getName方法獲取構造方法的名稱,getParameterTypes方法獲取構造方法的參數類型,getModifiers方法獲取構造方法的修飾符等信息。

  4. 最後,可以使用newInstance方法創建出新的對象,該方法接受一個可變參數列表,用於傳遞構造方法的參數值。如果構造方法沒有參數,則可以直接調用newInstance方法。

需要注意的是,使用反射API創建對象的效率較低,應該儘量避免在性能要求較高的場景中使用。

一般的構造方法的獲取和使用並沒有什麼特殊之處,需要特別說明的是對參數長度可變的構造方法和嵌套類(nested class)的構造方法的使用。

長度可變的參數 - 構造方法

如果一個構造方法聲明瞭長度可變的參數,需要使用對應的數組類型的 Class 對象來獲取該構造方法,因爲長度可變的參數實際上是通過數組來實現的。

使用反射 API 獲取參數長度可變的構造方法

例如,如果一個類 VarargsConstructor 的構造方法包含 String 類型的可變長度參數,調用getDeclaredConstructor 方法時需要使用 String[].class,否則會找不到該構造方法。在調用newInstance 方法時,需要將作爲實際參數的字符串數組先轉換爲 Object 類型,以避免方法調用時的歧義,這樣編譯器就知道將該字符串數組作爲一個可變長度的參數來傳遞。


public class VarargsConstructor {
	public VarargsConstructor(String... names) {}
}

public void useVarargsConstructor() throws Exception { 
	Constructor<VarargsConstructor> constructor = VarargsConstructor.class.
		getDeclaredConstructor(String[].class);
	constructor.newInstance((Object) new String[]{"A", "B", "C"});
}

獲取嵌套類的構造方法時,需要區分靜態和非靜態兩種情況。
在這裏插入圖片描述靜態嵌套類,可以按照一般的方式來使用。

非靜態嵌套類,其特殊之處在於它的對象實例中都有一個隱含的對象引用,指向包含它的外部類對象。這個隱含的對象引用的存在,使得非靜態嵌套類中的代碼可以直接引用外部類中包含的私有域和方法。因此,在獲取非靜態嵌套類的構造方法時,類型參數列表的第一個值必須是外部類的 Class 對象。

例如,對於非靜態嵌套類 NestedClass,獲取其構造方法時需要傳入外部類的 Class 對象作爲第一個參數,以便在創建新對象時傳遞外部對象的引用。

static class StaticNestedClass {
	public StaticNestedClass(String name) {}
}
class NestedClass {
	public NestedClass(int count) {}
}
public void useNestedClassConstructor() throws Exception {
	Constructor< StaticNestedClass> sncc = StaticNestedClass.class. getDeclaredConstructor(String.class);
	sncc.newInstance("Alex");
	Constructor<NestedClass> ncc = NestedClass.class.getDeclaredConstructor(ConstructorUsage.class, int.class);
	NestedClass ic = ncc.newInstance(this, 3);
}

獲取Field域

通過反射 API,可以獲取類中的域(field),包括公開的靜態域和對象中的實例域。獲取表示域的 java.lang.reflect.Field 類的對象之後,就可以獲取和設置域的值。與獲取構造方法的方法類似,Class 類中也有 4 個方法用來獲取域,分別是 getFields、getField、getDeclaredFields 和 getDeclaredField。
在這裏插入圖片描述

  • getFields 方法返回公開的靜態域和對象中的實例域;
  • getField 方法返回指定名稱的公開的靜態域或對象中的實例域;
  • getDeclaredFields 方法返回類中所有的域,包括私有的靜態域和對象中的實例域;
  • getDeclaredField 方法返回指定名稱的域,包括私有的靜態域和對象中的實例域。
使用反射 API 獲取和使用靜態域和實例域

獲取和使用靜態域和實例域的示例,兩者的區別在於使用靜態域時不需要提供具體的對象實例,使用 null 即可

Field 類中除了操作 Object 的 get 和 set 方法之外,還有操作基本類型的對應方法,包括 getBoolean / setBoolean、getByte / setByte、getChar / setChar、getDouble / setDouble、getFloat / setFloat、getInt / setInt 和 getLong / setLong 等

public void useField() throws Exception {
	Field fieldCount = FieldContainer.class.getDeclaredField("count");
	fieldCount.set(null, 3);
	Field fieldName = FieldContainer.class.getDeclaredField("name"); 
	FieldContainer fieldContainer = new FieldContainer(); 
	fieldName.set(fieldContainer, "Bob");
}

總的來說,獲取和設置類中的公開域比較簡單,但是無法通過反射 API 獲取或操作私有域。

獲取Method方法

最常使用反射 API 的場景是獲取對象中的方法,並在運行時調用該方法。Class 類中有 4 個方法用來獲取方法,分別是 getMethods、getMethod、getDeclaredMethods 和 getDeclaredMethod。這些方法的作用類似於獲取構造方法和域的對應方法。通過獲取表示方法的 java.lang.reflect.Method 類的對象,可以查詢該方法的詳細信息,例如方法的參數和返回值的類型等。使用 invoke 方法可以傳入實際參數並調用該方法。

獲取和調用對象中的公開和私有方法的示例
public void useMethod() throws Exception { 		
	MethodContainer mc = new MethodContainer();
	Method publicMethod = MethodContainer.class.getDeclaredMethod("publicMethod");
	publicMethod.invoke(mc);
	Method privateMethod = MethodContainer.class.getDeclaredMethod("privateMethod");
	privateMethod.setAccessible(true);
	privateMethod.invoke(mc);
}

需要注意的是,在調用私有方法之前,需要先調用 Method 類的setAccessible方法來設置可以訪問的權限。與構造方法和域不同的是,通過反射 API 可以獲取到類中的私有方法。

操作數組

利用反射API對數組進行操作的方式有所不同於一般的Java對象。需要使用java.lang.reflect.Array這個實用工具類來實現。該類提供了創建數組和操作數組元素的方法。newInstance方法用來創建新的數組。第一個參數是數組中元素的類型,後面的參數是數組的維度信息。

String[] names = ( Array.newInstance(int.class, 3, 3, 3);
double[][][] arrays= (double[][][]) Array.newInstance(double[][].class, 2, 2);
使用反射 API 操作數組

例如,可以使用下面的示例代碼創建一個長度爲10的一維String數組和一個3x3x3的三維數組:

public void useArray() {
	String[] names = (String[]) Array.newInstance(String.class, 10);
	names[0] = "Hello"; 
	Array.set(names, 1, "World");
	String str = (String) Array.get(names, 0);
	int[][][] matrix1 = (int[][][]) Array.newInstance(int.class, 3, 3, 3);
	matrix1[0][0][0] = 1;
	int[][][] matrix2 = (int[][][]) Array.newInstance(int[].class, 3, 4);
	matrix2[0][0] = new int[10]; 
	matrix2[0][1] = new int[3]; 
	matrix2[0][0][1] = 1;
}

需要注意的是,儘管在創建時只聲明瞭兩個維度,但是matrix2實際上也是一個三維數組,因爲它的元素類型是double。

訪問權限與異常處理

使用反射 API 可以繞過 Java 語言中默認的訪問控制權限,例如訪問在另一個類中聲明的私有方法。這是通過調用繼承自 java.lang.reflect.AccessibleObject 的 setAccessible 方法來實現的。在使用 invoke 方法調用方法時,如果方法本身拋出異常,invoke 方法會拋出 InvocationTargetException 異常來表示這種情況。可以通過 InvocationTargetException 異常的 getCause 方法獲取真正的異常信息來進行調試。

在 Java 7 中,所有與反射操作相關的異常類都添加了一個新的父類 java.lang.ReflectiveOperationException,可以直接捕獲這個新的異常。

內容總結

Java反射技術允許程序在運行時動態地獲取類的信息、調用類的方法、訪問類的屬性等,從而提高程序的靈活性和可擴展性。它可以獲取類的名稱、包名、父類、接口、構造方法、方法、屬性等信息,創建對象,調用方法,訪問屬性,實現動態代理等功能。Java反射技術在框架開發、ORM框架、動態代理、單元測試等方面都有着重要的應用。但是,由於使用反射技術需要額外的開銷,因此在性能要求較高的場景下,應該儘量避免使用。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章