Java的反射機制

真正的飛魚發表於2023-05-05

介紹反射機制

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 反射

相關文章