java-recipes

ホーム リフレクション › R-01

R-01: リフレクション基本

java.lang.reflect パッケージを使って、 実行時にクラスの情報を取得したり、private メンバーへアクセスしたり、メソッドを動的に呼び出す方法を学びます。

リフレクションとは

リフレクション(Reflection)とは、プログラムの実行中(ランタイム)に自分自身のクラス構造を調べたり、 変更したりする仕組みです。通常はコンパイル時に決まっているクラス名・フィールド・メソッドを、 実行時に動的に取得・操作できます。

フレームワークやライブラリの内部では広く使われています。たとえば Spring の DI(依存性注入)、 Jackson の JSON シリアライズ・デシリアライズ、JUnit のテストメソッド検出などは、 いずれもリフレクションによって実現されています。

主な用途

  • フレームワーク・DI: クラス名の文字列から動的にオブジェクトを生成する (例: 設定ファイルに書いたクラス名をインスタンス化する)
  • シリアライズ: オブジェクトのフィールドを列挙して JSON や XML に変換する
  • テスト: private メソッドをテストするため、テストコードからアクセスする
  • デバッグ・ツール: オブジェクトの状態を実行時に検査・出力する

サンプルコード

ReflectionBasicSample.java
import java.lang.reflect.*;
import java.util.Arrays;

public class ReflectionBasicSample {

    // サンプル対象クラス
    static class Person {
        private String name;
        private int age;
        public String email;

        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public String getName() { return name; }
        public int getAge() { return age; }

        private String secret() {
            return "秘密のメソッド: " + name;
        }

        @Override
        public String toString() {
            return "Person{name=" + name + ", age=" + age + "}";
        }
    }

    public static void main(String[] args) throws Exception {
        System.out.println("=== クラス情報取得 ===");
        Class<?> clazz = Class.forName("ReflectionBasicSample$Person");
        System.out.println("クラス名: " + clazz.getSimpleName());
        System.out.println("パッケージ: " + clazz.getPackage());

        System.out.println("\n=== フィールド一覧 ===");
        for (Field field : clazz.getDeclaredFields()) {
            System.out.println("  " + Modifier.toString(field.getModifiers())
                + " " + field.getType().getSimpleName() + " " + field.getName());
        }

        System.out.println("\n=== メソッド一覧 ===");
        for (Method method : clazz.getDeclaredMethods()) {
            System.out.println("  " + Modifier.toString(method.getModifiers())
                + " " + method.getReturnType().getSimpleName() + " " + method.getName() + "()");
        }

        System.out.println("\n=== インスタンス生成 ===");
        Constructor<?> constructor = clazz.getDeclaredConstructor(String.class, int.class);
        Object person = constructor.newInstance("田中太郎", 25);
        System.out.println(person);

        System.out.println("\n=== private フィールドへのアクセス ===");
        Field nameField = clazz.getDeclaredField("name");
        nameField.setAccessible(true); // private フィールドにアクセス可能にする
        String name = (String) nameField.get(person);
        System.out.println("name (private): " + name);

        System.out.println("\n=== private メソッドの呼び出し ===");
        Method secretMethod = clazz.getDeclaredMethod("secret");
        secretMethod.setAccessible(true);
        String result = (String) secretMethod.invoke(person);
        System.out.println(result);

        System.out.println("\n=== public メソッドの動的呼び出し ===");
        Method getNameMethod = clazz.getMethod("getName");
        System.out.println("getName(): " + getNameMethod.invoke(person));
    }
}

Java 8 からリフレクション API が利用できます。getDeclaredFields() / getDeclaredMethods() で private メンバーも含めて取得でき、setAccessible(true) でアクセス制限を解除できます。

よくあるミス・注意点

⚠️ setAccessible(true) はセキュリティポリシーに違反する可能性がある

setAccessible(true) を使うと private フィールドやメソッドにアクセスできますが、 Java モジュールシステム(Java 9 以降の JPMS)や SecurityManager が有効な環境ではInaccessibleObjectExceptionSecurityException がスローされることがあります。 本番コードでは設計を見直し、public API を使うようにしましょう。

⚠️ リフレクションは通常の呼び出しより遅い

リフレクションによるメソッド呼び出しは、通常の直接呼び出しに比べて数倍〜数十倍遅くなることがあります。 JIT コンパイラによる最適化も効きにくいため、ループ内での頻繁なリフレクション呼び出しは避けましょう。 一度取得した Method オブジェクトやField オブジェクトはキャッシュして再利用することを検討してください。

⚠️ Class.forName() はクラスローダーに依存する

Class.forName("クラス名") は 完全修飾クラス名(パッケージ名を含む名前)で指定する必要があります。 内部クラスの場合は OuterClass$InnerClass のように$ で区切ります。 クラスが見つからない場合は ClassNotFoundException がスローされます。

テストする観点

  • getDeclaredFields() で取得したフィールド名・型・修飾子が正しいこと
  • setAccessible(true) の後に private フィールドの値を取得できること
  • getDeclaredMethod() + invoke() で private メソッドを呼び出せること
  • コンストラクタ経由でインスタンスを生成し、メソッドを呼び出した結果が正しいこと
  • 存在しないフィールド名を指定したときにNoSuchFieldException がスローされること(境界値)
  • 存在しないメソッド名を指定したときにNoSuchMethodException がスローされること(境界値)

GitHub でソースコードを見る →