天天看點

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

作者:IT技術控
帶你攻破你很可能存在的Java技術盲點之動态性技術原理指南

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

本系列技術專題的相關技術指南主要有以下三個方面:

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

程式設計語言的類型

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

整體的程式設計語言分為三大類:靜态類型語言和動态類型語言、半靜态半動态類型語言。

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

靜态類型語言

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

動态類型語言

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

技術核心方向

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

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

反射API

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

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

java複制代碼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。

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

擷取構造器

可以通過反射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 類型,以避免方法調用時的歧義,這樣編譯器就知道将該字元串數組作為一個可變長度的參數來傳遞。

java複制代碼
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"});
}
           

擷取嵌套類的構造方法時,需要區分靜态和非靜态兩種情況。

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

靜态嵌套類,可以按照一般的方式來使用。

非靜态嵌套類,其特殊之處在于它的對象執行個體中都有一個隐含的對象引用,指向包含它的外部類對象。這個隐含的對象引用的存在,使得非靜态嵌套類中的代碼可以直接引用外部類中包含的私有域和方法。是以,在擷取非靜态嵌套類的構造方法時,類型參數清單的第一個值必須是外部類的 Class 對象。

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

java複制代碼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。

帶你攻破你很可能存在的Java技術盲點之動态性技術原理指南
  • getFields 方法傳回公開的靜态域和對象中的執行個體域;
  • getField 方法傳回指定名稱的公開的靜态域或對象中的執行個體域;
  • getDeclaredFields 方法傳回類中所有的域,包括私有的靜态域和對象中的執行個體域;
  • getDeclaredField 方法傳回指定名稱的域,包括私有的靜态域和對象中的執行個體域。

使用反射 API 擷取和使用靜态域和執行個體域

擷取和使用靜态域和執行個體域的示例,兩者的差別在于使用靜态域時不需要提供具體的對象執行個體,使用 null 即可。

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

java複制代碼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 方法可以傳入實際參數并調用該方法。

擷取和調用對象中的公開和私有方法的示例

java複制代碼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方法用來建立新的數組。第一個參數是數組中元素的類型,後面的參數是數組的次元資訊。

java複制代碼String[] names = ( Array.newInstance(int.class, 3, 3, 3);
double[][][] arrays= (double[][][]) Array.newInstance(double[][].class, 2, 2);
           

使用反射 API 操作數組

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

java複制代碼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架構、動态代理、單元測試等方面都有着重要的應用。但是,由于使用反射技術需要額外的開銷,是以在性能要求較高的場景下,應該盡量避免使用。

繼續閱讀