1. 什麼是反射
Java 的反射是一種機制,允許程式在執行期間檢查甚至改變自身的結構。就好比你能夠往自己內部看看並問:「我有哪些欄位與方法?我的建構子是什麼?我到底是什麼類別?」——甚至還能呼叫自己的私有方法。
形式化定義:
反射是一組 API,允許在程式執行時取得類、介面、欄位、方法與建構子的資訊,並對它們進行操作。
為什麼需要反射?
- 框架:Spring、Hibernate、JUnit 使用反射達成「魔法」:自動建立物件、依賴注入、依名稱或依註解呼叫方法等。
- 序列化:將物件轉成 JSON/XML 再轉回——需要得知欄位並能存取它們。
- 測試:自動尋找並呼叫帶有註解的方法(例如 @Test)。
- 動態載入類別:在執行期依名稱載入類別(外掛、驅動程式)。
- IDE 與程式碼分析:自動完成、檢查、重構。
實例
public class Person {
private String name;
private int age;
public void sayHello() {
System.out.println("你好,我叫 " + name);
}
}
借助反射,我們可以在執行期得知類別 Person 具備欄位 name 與 age,以及方法 sayHello。更進一步,還能修改私有欄位的值,或依名稱呼叫方法。
一點幽默
可以說,反射就像在《駭客任務》中:你意識到你的程式碼都是可被即時檢查與改變的資料。只是別忘了那顆關於安全性的紅色藥丸!
2. 類別 Class:反射的核心
在 Java 中,每個物件與每個型別在執行期都對應到一個特殊的 Class 物件。這個物件就是該型別的「中繼資訊」。類別 java.lang.Class<T> 是進入反射世界的核心入口。
如何取得 Class 物件?
有幾種方式:
- 透過 .class
若在編譯期已知類型,這是最直接且安全的方式:Class<Person> personClass = Person.class; - 透過物件:getClass()
如果你已有實例:
此處的 clazz 是描述物件 p 實際類別的 Class 物件。Person p = new Person(); Class<?> clazz = p.getClass(); - 透過字串:Class.forName()
當類名只在執行期才知道時:
適合載入外掛、驅動程式與其他動態情境。Class<?> clazz = Class.forName("com.example.Person");
範例:取得類名
Person p = new Person();
Class<?> clazz = p.getClass();
System.out.println(clazz.getName()); // 輸出:Person 或 com.example.Person
表格:獲取 Class 物件的主要方式
| 如何取得 | 何時使用 | 範例 |
|---|---|---|
|
類型在編譯期已知時 | |
|
已有物件時 | |
|
類名在執行期才已知時 | |
示意圖:物件與其 Class
+-------------------+
| Person p |
+-------------------+
|
v
+-------------------+
| p.getClass() |
+-------------------+
|
v
+-------------------+
| Class<Person> |
+-------------------+
透過 Class 檢查型別
有時需要判斷某物件是否屬於特定類別:
if (p.getClass() == Person.class) {
System.out.println("這就是 Person!");
}
若考慮繼承——使用一般的 instanceof 運算子:
if (p instanceof Person) {
// 如常運作
}
3. 什麼時候需要反射,什麼時候不需要
能力越大,責任越大。反射是強大的工具,但只應在確有必要時使用。
必須使用反射的情境
- 框架與函式庫:Spring、Hibernate、JUnit、Jackson——自動化、「魔法」,減少「手寫」程式碼。
- 序列化:需要了解物件欄位,才能轉成 JSON/XML。
- 外掛與擴充:類別動態載入,其結構事先未知。
為何不要濫用反射
- 失去型別安全:錯誤會在執行期才浮現。
- 維護困難:程式碼變得不那麼直觀。
- 效能:比直接呼叫更慢。
- 安全性:可能繞過封裝,暴露私有資料。
範例:不需要反射的情況
p.sayHello();
範例:反射不可或缺的時候
你在撰寫一個迷你測試框架:使用者以 @Test 標註方法,程式需要自動找到並呼叫它們——沒有反射就做不到。
4. 示範:認識 Class
範例:輸出類名與其超類
public class ReflectionDemo {
public static void main(String[] args) {
String s = "Hello, reflection!";
Class<?> clazz = s.getClass();
System.out.println("類名: " + clazz.getName());
System.out.println("簡單類名: " + clazz.getSimpleName());
System.out.println("套件: " + clazz.getPackageName());
System.out.println("超類: " + clazz.getSuperclass().getName());
}
}
結果:
類名: java.lang.String
簡單類名: String
套件: java.lang
超類: java.lang.Object
範例:取得基本型別的 Class
Class<Integer> intClass = int.class;
System.out.println(intClass.getName()); // int
Class<?> doubleClass = double.class;
System.out.println(doubleClass.getName()); // double
範例:取得陣列的 Class
int[] arr = new int[10];
Class<?> arrClass = arr.getClass();
System.out.println(arrClass.getName()); // [I(陣列的特殊格式)
範例:檢查是否為陣列型別
if (arrClass.isArray()) {
System.out.println("這是陣列!");
}
5. 實作:小程式「這是什麼類?」
我們來寫個程式,讀入完整類名(例如 "java.util.ArrayList"),並輸出其基本資訊。
import java.util.Scanner;
public class ClassInfoPrinter {
public static void main(String[] args) throws Exception {
Scanner scanner = new Scanner(System.in);
System.out.print("請輸入完整類名: ");
String className = scanner.nextLine();
Class<?> clazz = Class.forName(className);
System.out.println("類名: " + clazz.getName());
System.out.println("套件: " + clazz.getPackageName());
System.out.println("超類: " + clazz.getSuperclass().getName());
Class<?>[] interfaces = clazz.getInterfaces();
System.out.print("實作的介面: ");
for (Class<?> i : interfaces) {
System.out.print(i.getName() + " ");
}
System.out.println();
}
}
執行範例:
請輸入完整類名: java.util.ArrayList
類名: java.util.ArrayList
套件: java.util
超類: java.util.AbstractList
實作的介面: java.util.List java.util.RandomAccess java.lang.Cloneable java.io.Serializable
關於 Class 與反射的有趣事實
- 在 JVM 中,每個已載入的型別都有一個共同代表——Class 物件。同一型別的所有實例共享同一個 Class<T>。
- 可以判斷型別的種類:isInterface()、isEnum()、isArray()、isPrimitive() 等。
- 若在 Class.forName 中指定錯誤的名稱,會拋出 ClassNotFoundException。
- 透過反射可以建立物件,即使在編譯期不知道其類別——我們會在下一講討論這一點。
6. 初學反射時的常見錯誤
錯誤 1:以為反射很快。
其實反射比一般的方法呼叫與欄位存取更慢。不要在每件小事上都使用它。
錯誤 2:嘗試為不存在的類別取得 Class。
如果類名寫錯,會得到 ClassNotFoundException。請妥善處理此例外。
錯誤 3:把類別物件和實例搞混。
Class 物件描述的是型別的結構,本身不是該型別的實例。
錯誤 4:不必要地使用反射。
能用一般程式碼解決就別用反射。反射適合用在動態場景、框架、外掛等任務。
錯誤 5:不小心操作私有欄位與方法。
反射能繞過封裝,這可能導致錯誤與安全問題,尤其是在大型專案中。
GO TO FULL VERSION