概要

リフレクションは、コンパイル時には決まらないクラスやメソッドを実行時に操作するための仕組みです。普段の業務コードで直接使う場面はそれほど多くありませんが、共通ライブラリの設計、簡易テストランナーの構築、設定値の動的バインドなど「もう一段奥の仕組み」を作るときに避けて通れない技術でもあります。この記事では、Class オブジェクトの取得からフィールド・メソッドの列挙、コンストラクタ経由のインスタンス生成、private メンバーへのアクセスまで、リフレクションの基本操作を動作するコードとともに整理します。実務では「使える」と「使うべき」の線引きが重要になるため、パフォーマンスやセキュリティの観点での注意点も合わせて扱います。

使いどころ

プラグイン機構で設定ファイルに書かれたクラス名から Class.forName でインスタンスを動的に生成する

テストコードから private メソッドを呼び出し、内部ロジックの境界値を直接検証する

汎用的なオブジェクトダンプ処理で、任意のクラスのフィールド一覧と値をログに出力する

コード例

ReflectionBasicDemo.java
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;

public class ReflectionBasicDemo {

    static class Person {
        private String name;
        private int age;

        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 + "(" + age + ")";
        }

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

    public static void main(String[] args) throws Exception {

        var clazz = Person.class;
        System.out.println("クラス名: " + clazz.getSimpleName());

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

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

        var constructor = clazz.getDeclaredConstructor(String.class, int.class);
        var person = constructor.newInstance("田中太郎", 30);
        System.out.println("\n生成: " + person);

        var nameField = clazz.getDeclaredField("name");
        nameField.setAccessible(true);
        System.out.println("private name: " + nameField.get(person));

        var secretMethod = clazz.getDeclaredMethod("secret");
        secretMethod.setAccessible(true);
        System.out.println("private secret(): " + secretMethod.invoke(person));

        var getNameMethod = clazz.getMethod("getName");
        System.out.println("getName(): " + getNameMethod.invoke(person));

        System.out.println("\n=== public メソッド(Object 由来を除外)===");
        Arrays.stream(clazz.getMethods())
            .filter(m -> m.getDeclaringClass() != Object.class)
            .forEach(m -> System.out.println("  " + m.getName() + "()"));
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

var による型推論で記述が簡潔になる。record クラスでは isRecord() や getRecordComponents() でコンポーネント情報を直接取得できる。

Java 17
// Java 17: var + record 対応の API を活用
var clazz = Person.class;
System.out.println("record: " + clazz.isRecord());

for (var component : clazz.getRecordComponents()) {
    System.out.println(component.getType().getSimpleName()
        + " " + component.getName());
}

var person = clazz.getDeclaredConstructor(String.class, int.class)
    .newInstance("田中", 25);

Library Comparison

標準 API(java.lang.reflect)フィールド列挙や動的インスタンス生成など、基本的なリフレクション操作を行うとき。API が低レベルで冗長になりやすい。アクセス制御の回避にはモジュール設定が必要になる場合がある。
Spring ReflectionUtilsSpring プロジェクト内で private フィールドの読み書きや特定アノテーション付きメソッドの検索を簡潔に行いたいとき。Spring Framework への依存が前提。単体ライブラリとしては提供されておらず、非 Spring プロジェクトには持ち込みにくい。
MethodHandle(java.lang.invoke)リフレクションと同等の動的呼び出しを、より高速に行いたいとき。API の学習コストが高く、可読性も下がる。頻繁に呼び出すホットパスでなければ reflect API で十分なことが多い。

注意点

setAccessible(true) は Java 9 以降のモジュールシステムで制限される場合がある。--add-opens をつけて回避できるが、ライブラリ提供時は InaccessibleObjectException のハンドリングを入れておくこと

リフレクション経由の呼び出しは通常のメソッド呼び出しより数倍遅い。ホットパスで毎回 getDeclaredMethod を呼ぶのは避け、Method オブジェクトをキャッシュするか MethodHandle への切り替えを検討する

getDeclaredFields と getFields は取得範囲が異なる。前者は宣言クラスの全フィールド(private 含む)、後者は継承を含む public フィールドのみを返す。意図と異なるメソッドを使うと必要なフィールドが取れない

Class.forName に存在しないクラス名を渡すと ClassNotFoundException になる。設定ファイルからクラス名を読む場合はタイポや classpath 不足への備えが必要

FAQ

リフレクションで private メソッドを呼ぶのはテスト以外でも許容されますか。

フレームワーク内部や共通基盤では使われますが、業務ロジックで常用するのは避けるのが無難です。カプセル化を崩すとリファクタリング時に追従が難しくなります。

getDeclaredMethods と getMethods の違いは何ですか。

getDeclaredMethods はそのクラスで宣言されたメソッド(private 含む)を返します。getMethods は継承チェーン上の public メソッドをすべて返しますが、private は含みません。

Java 9 以降のモジュールシステムで setAccessible が失敗する場合の対処法は。

起動オプションに --add-opens でモジュールとパッケージを指定します。ライブラリ側では InaccessibleObjectException を catch し、フォールバック処理を用意しておくと安全です。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます