CodeGym /課程 /JAVA 25 SELF /透過反射建立物件

透過反射建立物件

JAVA 25 SELF
等級 62 , 課堂 3
開放

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+)中也會受到額外的存取限制。

各種建立物件方式的比較表

方式 速度 型別安全 何時使用
new User()
最快 完整 只要能用就用
Constructor.newInstance()
較慢 喪失 框架、外掛
MethodHandle
中等 部分 重視效能的程式庫
工廠 快速 完整 彈性建立物件

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 註解的方法、建立測試實例並呼叫方法。
  • SpringDI、建立 Bean、自動裝配。
  • JacksonGson:物件欄位的序列化/反序列化。
  • 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) 就像「闖入別人冰箱」。僅在確有必要時使用,並考量安全需求。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION