1. 透過反射建立物件
有時我們需要在編譯時不知型別的情況下建立物件。比如類別名稱來自設定檔,或我們在撰寫通用框架(序列化器、DI 容器)。在一般程式碼中我們會寫:
User user = new User("Ivan", 25);
但如果我們不知道類別叫做 User,也不知道其建構子的參數呢?這時反射就派上用場。
過時的做法: Class.newInstance()
早期在 Java 中有個方法 Class.newInstance(),可透過無參數的公開建構子建立物件:
Class<?> clazz = Class.forName("com.example.User");
Object obj = clazz.newInstance();
重要: 此方法自 Java 9 起標記為 deprecated,不建議使用。它無法選擇建構子、會掩蓋錯誤原因,且只適用於無參數的 public 建構子。
現代做法:使用建構子
實務上並不一定都有 public 無參數建構子。較新的做法是先透過 getConstructor(...) 或 getDeclaredConstructor(...) 取得所需建構子,接著呼叫它的 newInstance(...)。
範例:建立帶參數的物件
public class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
}
import java.lang.reflect.Constructor;
Class<?> clazz = Class.forName("User");
// 取得帶有 String、int 參數的建構子
Constructor<?> constructor = clazz.getConstructor(String.class, int.class);
// 建立物件並傳入參數
Object user = constructor.newInstance("Ivan", 25);
System.out.println(user); // 若已定義 toString
這裡發生了什麼?
- 依簽章取得所需的建構子。
- 呼叫它的 newInstance(...),傳入引數。
- 得到一個 Object(若已知實際型別,可轉型為 User)。
如果建構子是 private?
Constructor<?> constructor = clazz.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true); // 魔法!現在可以呼叫私有建構子。
Object user = constructor.newInstance("Ivan", 25);
注意: 不宜濫用——這會破壞封裝。在模組化應用(Java 9+)中也會受到額外的存取限制。
各種建立物件方式的比較表
| 方式 | 速度 | 型別安全 | 何時使用 |
|---|---|---|---|
|
最快 | 完整 | 只要能用就用 |
|
較慢 | 喪失 | 框架、外掛 |
|
中等 | 部分 | 重視效能的程式庫 |
| 工廠 | 快速 | 完整 | 彈性建立物件 |
2. 透過反射呼叫方法
你可以呼叫任何物件的方法,即使在編譯時不知道其名稱,或它是 private。
取得方法
import java.lang.reflect.Method;
Class<?> clazz = user.getClass();
// 依名稱與參數型別取得 public 方法
Method method = clazz.getMethod("getName"); // 無參數
Object result = method.invoke(user); // 呼叫無參數方法
System.out.println(result); // 會輸出使用者名稱
呼叫帶參數的方法
Method setName = clazz.getMethod("setName", String.class);
setName.invoke(user, "Petr"); // 設定新名稱
呼叫私有方法
Method secret = clazz.getDeclaredMethod("secretMethod", int.class);
secret.setAccessible(true); // 解除存取限制
Object secretResult = secret.invoke(user, 123);
重要: 所有參數都以物件陣列(varargs)傳遞。若方法有回傳值,結果會以 Object 傳回。
3. 透過反射存取欄位
有時需要讀取或修改欄位的值,即使它是 private(例如在序列化或測試時)。
取得欄位
import java.lang.reflect.Field;
Class<?> clazz = user.getClass();
Field ageField = clazz.getDeclaredField("age");
ageField.setAccessible(true); // 若欄位不是 public
// 讀取值
Object age = ageField.get(user);
System.out.println("年齡:" + age);
// 修改值
ageField.set(user, 42);
System.out.println("新的年齡:" + ageField.get(user));
操作靜態欄位
如果欄位是靜態的,請在 get/set 中將物件參數設為 null:
Field staticField = clazz.getDeclaredField("counter");
staticField.setAccessible(true);
staticField.set(null, 100); // 對 static 欄位不需要傳入物件
4. 限制與例外
使用反射時會拋出許多受檢例外(checked)。最常見的有:
- ClassNotFoundException — 找不到該名稱的類別。
- NoSuchMethodException — 找不到具有該簽章的建構子或方法。
- NoSuchFieldException — 找不到欄位。
- IllegalAccessException — 無權存取成員(例如未 setAccessible(true) 的 private 方法)。
- InstantiationException — 類別是抽象或介面;無法建立實例。
- InvocationTargetException — 被呼叫的建構子或方法內部發生錯誤。
處理範例:
try {
// ... 你的反射程式碼 ...
} catch (ReflectiveOperationException e) {
e.printStackTrace();
}
若不需要細節,直接捕捉共同的超類別 ReflectiveOperationException(Java 7+)更方便。
5. 實作:迷你程式「動態實例化器」
用於實驗的範例類別
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
private void sayHello() {
System.out.println("嗨,我叫 " + name + ",我今年 " + age + " 歲。");
}
}
動態建立與操作
import java.lang.reflect.*;
public class ReflectionDemo {
public static void main(String[] args) {
try {
// 1. 依名稱取得 Class 物件
Class<?> clazz = Class.forName("Person");
// 2. 取得建構子並建立物件
Constructor<?> constructor = clazz.getConstructor(String.class, int.class);
Object person = constructor.newInstance("Alisa", 30);
// 3. 呼叫私有方法 sayHello
Method sayHello = clazz.getDeclaredMethod("sayHello");
sayHello.setAccessible(true);
sayHello.invoke(person); // 嗨,我叫 Alisa,我今年 30 歲。
// 4. 修改私有欄位 name
Field nameField = clazz.getDeclaredField("name");
nameField.setAccessible(true);
nameField.set(person, "Bob");
// 5. 再次呼叫 sayHello
sayHello.invoke(person); // 嗨,我叫 Bob,我今年 30 歲。
} catch (ReflectiveOperationException e) {
e.printStackTrace();
}
}
}
請注意:
- 整個過程都不需要在程式中直接知道 Person 的型別。
- 你可以修改私有欄位並呼叫私有方法(前提是 JVM 的安全政策與模組設定允許)。
6. 這與實際應用有何關聯?
反射是許多熱門程式庫與框架的基礎:
- JUnit:搜尋帶有 @Test 註解的方法、建立測試實例並呼叫方法。
- Spring:DI、建立 Bean、自動裝配。
- Jackson、Gson:物件欄位的序列化/反序列化。
- Hibernate:對實體欄位的存取、代理與延遲載入。
7. 視覺化流程:透過反射如何建立物件
flowchart TB
A["類名 (String)"] --> B["Class.forName"]
B --> C["Class<?>"]
C --> D["getConstructor(...)"]
D --> E["Constructor<?>"]
E --> F["newInstance(...)"]
F --> G["Object"]
8. 使用反射時的常見錯誤
錯誤一:建構子不正確。 以錯誤的簽章請求建構子,例如 getConstructor(String.class),但實際只有帶兩個參數的建構子——會得到 NoSuchMethodException。務必檢查簽章!
錯誤二:存取權限不足。 呼叫 private 的建構子/方法而未 setAccessible(true),會導致 IllegalAccessException。且別忘了:在 Java 9+ 中,模組系統可能施加額外限制。
錯誤三:型別問題。 透過反射傳遞與回傳的都是 Object。不正確的轉型會造成 ClassCastException。
錯誤四:未處理的例外。 反射會拋出許多受檢例外。請處理它們,例如捕捉通用的 ReflectiveOperationException,否則程式無法編譯。
錯誤五:破壞封裝。 濫用 setAccessible(true) 就像「闖入別人冰箱」。僅在確有必要時使用,並考量安全需求。
GO TO FULL VERSION