Java 的反射機制允許在程式運行期間,藉助反射 API 獲取類的內部信息,並能直接操作對象的內部屬性及方法。 ...
介紹反射機制
Java 的反射機制允許在程式運行期間,藉助反射 API 獲取類的內部信息,並能直接操作對象的內部屬性及方法。
Java 反射機制提供的功能:
- 在運行時,使用反射分析類的能力,獲取有關類的一切信息(類所在的包、類實現的介面、標註的註解、類的數據域、類的構造器、類的方法等)
- 在運行時,使用反射分析對象,設置實例域的值,查看實例域的值。
- 反射機制允許你調用任意方法(類的構造器方法、類的成員方法 等)
反射是一種功能強大且複雜的機制。使用反射機制的主要人員是工具構造者,而不是應用程式員。
Class 類
在程式運行期間,Java 運行時系統始終為所有的對象維護一個被稱為運行時的類型標識。這個信息跟蹤著每個對象所屬的類。虛擬機利用運行時類型信息選擇相應的方法執行。
然而,可以通過專門的 Java 類訪問這些信息。保存這些信息的類被稱為 Class。Object 類中的 getClass() 方法將會返回一個 Class 類型的實例。
如同用一個 Employee 對象表示一個特定的雇員屬性一樣,一個 Class 對象將表示一個特定類的屬性。
虛擬機為每個類型管理一個 Class 對象。因此,可以利用 == 運算符實現兩個 Class 對象比較的操作。
// 獲得 Class 對象的多種方式:
public static void main(String[] args) {
// 方式 1
// 如果 T 是任意的 Java 類型 (或 void 關鍵字), T.class 將代表匹配的 Class 對象。
Class<Person> clazz1 = Person.class;
// 方式 2
Person person = new Person();
Class clazz2 = person.getClass();
// 方式 3
try {
Class clazz3 = Class.forName("類的路徑");
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
// 方式4
// 獲取到 ClassLoader(這裡獲取到的是:AppClassLoader)
ClassLoader classLoader = ClassLoader.getSystemClassLoader();
try {
Class clazz4 = classLoader.loadClass("類的路徑");
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
還有一個很有用的方法:Class 類的 newlnstance(),可以用這個方法來動態地創建一個類的實例。newlnstance() 方法調用預設的構造器(沒有參數的構造器)初始化新創建的對象。如果這個類沒有預設的構造器,就會拋出一個 InstantiationException 異常。
將 Class 類的 forName() 方法與 Class 類的 newlnstance() 方法配合起來使用,可以根據存儲在字元串中的類名創建一個對象。
public static void main(String[] args) throws Exception {
String className = "java.util.Random";
Object object = Class.forName(className).newInstance();
}
如果需要以這種方式向希望按名稱創建的類的構造器提供參數,就不要使用上面那條語句,而必須使用 Constructor 類中的 newlnstance() 方法。
分析類的能力
在運行時,使用反射分析類的能力。
下麵簡要地介紹一下反射機制最重要的內容:檢查類的結構。在 java.lang.reflect 包中有三個類 Field、Method 和 Constructor 分別用於描述類的數據域、類的方法和類的構造器。
這三個類都有一個叫做 getName() 的方法,用來返回項目的名稱。
Field 類有一個 getType() 方法,用來返回描述數據域所屬類型的 Class 對象。
Method 類和 Constructor 類有能夠報告參數類型的方法,Method 類還有一個可以報告返回類型的方法。
這三個類還有一個叫做 getModifiers() 的方法,它將返回一個整型數值,用不同的位開關描述 public 和 static 這樣的修飾符使用狀況。另外, 還可以利用 java.lang.reflect 包中的 Modifier 類的靜態方法分析 getModifiers() 返回的整型數值。例如,可以使用 Modifier 類中的 isPublic()、isPrivate() 或 isFinal() 判斷方法或構造器是否是 public、private 或 final 的。我們需要做的全部工作就是調用 Modifier 類的相應方法,並對返回的整型數值進行分析,另外,還可以利用 Modifier.toString() 方法將修飾符列印出來。
Class 類的 getFields()、getMethods() 和 getConstructors() 方法將分別返回類中聲明的 public 域、public 方法和 public 構造器數組,其中包括父類的公有成員。
Class 類的 getDeclareFields()、getDeclareMethods() 和 getDeclaredConstructors() 方法將分別返回類中聲明的全部的數據域、全部的方法和全部的構造器,其中包括私有和受保護成員,但不包括父類的成員。
分析對象
在運行時,使用反射分析對象。
從前面一節中,已經知道如何查看任意對象的數據域的名稱和類型:
- 獲得對應的 Class 對象。
- 調用 Class 對象的 getDeclaredFields() 方法。
本節將進一步查看數據域的實際內容。當然,在編寫程式時,如果知道想要査看的數據域的名稱和類型,查看指定的數據域是一件很容易的事情。而利用反射機制可以查看在編譯時還不清楚的數據域。
查看數據域值的關鍵方法是 Field 類中的 get() 方法。如果 f 是一個 Field 類型的對象(例如,通過 getDeclaredFields() 得到的對象),obj 是某個包含 f 域的類的對象,f.get(obj) 將返回一個對象,其值為 obj 對象的 f 域的當前值。
當然,可以獲得就可以設置。調用 f.set(obj, value) 可以將 obj 對象的 f 域設置成新值。
public static void main(String[] args) {
Employee harry = new Employee("Harry Hacker", 35000, 10, 1, 1989);
Class cl = harry.getClass();
// the class object representing Employee
Field f = cl.getDeclaredField("name");
// the name field of the Employee class
Object v = f.get(harry);
// the value of the name field of the harry object, i .e., the String object "Harry Hacker"
}
實際上,上面這段代碼存在一個問題。由於 name 是一個私有域,所以 get() 方法將會拋出一個 illegalAccessException。只有利用 get() 方法才能得到可訪問域的值。除非擁有訪問許可權,否則 Java 安全機制只允許査看任意對象有哪些域,而不允許讀取它們的值。
反射機制的預設行為受限於 Java 的訪問控制。然而,如果一個 Java 程式沒有受到安全管理器的控制,就可以覆蓋訪問控制。為了達到這個目的,需要調用 Field、Method 或 Constructor 對象的 setAccessible() 方法。例如:
f.setAtcessible(true); // now OK to call f.get(harry);
setAccessible() 方法是 AccessibleObject 類中的一個方法,AccessibleObject 類是 Field、Method 和 Constructor 類的公共父類。這個特性是為調試、持久存儲和相似機制提供的。
調用任意方法
在 C 和 C++ 中,可以從函數指針執行任意函數。從錶面上看,Java 沒有提供方法指針,即將一個方法的存儲地址傳給另外一個方法,以便第二個方法能夠隨後調用它。事實上,Java 的設計者曾說過:方法指針是很危險的,並且常常會帶來隱患。他們認為 Java 提供的介面(interface)是一種更好的解決方案。然而,反射機制允許你調用任意方法。
為了能夠看到方法指針的工作過程,先回憶一下利用 Field 類的 get() 方法查看數據域值的過程。與之類似,在 Method 類中有一個 invoke() 方法,它允許調用包裝在當前 Method 對象中的方法。
可以使用 method 對象實現 C 語言中函數指針(或 C# 中的委派)的所有操作。同 C 一樣,這種程式設計風格並不太簡便,出錯的可能性也比較大。如果在調用方法的時候提供了一個錯誤的參數,那麼 invoke() 方法將會拋出一個異常。
另外, invoke() 方法的參數和返回值必須是 Object 類型的。這就意味著必須進行多次的類型轉換。這樣做將會使編譯器錯過檢查代碼的機會。因此,等到測試階段才會發現這些錯誤,找到並改正它們將會更加困難。
在進行類型轉換的過程中,編譯器無法檢查代碼中類型轉換的正確性,也就是無法保證轉換後的類型與原始類型是相容的。這樣就會增加程式出錯的可能性,並且如果出現錯誤的話,調試和修正也會更加困難。
不僅如此,使用反射獲得方法指針的代碼執行要比直接調用方法明顯慢一些。
有鑒於此,建議僅在必要的時候才使用 Method 對象,而最好使用介面以及 Java8 中的 lambda 表達式。
特別要重申:建議 Java 開發者不要使用 Method 對象的回調功能。使用介面進行回調會使得代碼的執行速度更快,更易於維護。
參考資料
《Java核心技術捲一:基礎知識》(第10版)第 5 章:繼承 5.7 反射
本文來自博客園,作者:真正的飛魚,轉載請註明原文鏈接:https://www.cnblogs.com/feiyu2/p/17375039.html