概要
リフレクションは、コンパイル時には決まらないクラスやメソッドを実行時に操作するための仕組みです。普段の業務コードで直接使う場面はそれほど多くありませんが、共通ライブラリの設計、簡易テストランナーの構築、設定値の動的バインドなど「もう一段奥の仕組み」を作るときに避けて通れない技術でもあります。この記事では、Class オブジェクトの取得からフィールド・メソッドの列挙、コンストラクタ経由のインスタンス生成、private メンバーへのアクセスまで、リフレクションの基本操作を動作するコードとともに整理します。実務では「使える」と「使うべき」の線引きが重要になるため、パフォーマンスやセキュリティの観点での注意点も合わせて扱います。
使いどころ
プラグイン機構で設定ファイルに書かれたクラス名から Class.forName でインスタンスを動的に生成する
テストコードから private メソッドを呼び出し、内部ロジックの境界値を直接検証する
汎用的なオブジェクトダンプ処理で、任意のクラスのフィールド一覧と値をログに出力する
コード例
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() + "()"));
}
}Version Coverage
var による型推論で記述が簡潔になる。record クラスでは isRecord() や getRecordComponents() でコンポーネント情報を直接取得できる。
// 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
注意点
setAccessible(true) は Java 9 以降のモジュールシステムで制限される場合がある。--add-opens をつけて回避できるが、ライブラリ提供時は InaccessibleObjectException のハンドリングを入れておくこと
リフレクション経由の呼び出しは通常のメソッド呼び出しより数倍遅い。ホットパスで毎回 getDeclaredMethod を呼ぶのは避け、Method オブジェクトをキャッシュするか MethodHandle への切り替えを検討する
getDeclaredFields と getFields は取得範囲が異なる。前者は宣言クラスの全フィールド(private 含む)、後者は継承を含む public フィールドのみを返す。意図と異なるメソッドを使うと必要なフィールドが取れない
Class.forName に存在しないクラス名を渡すと ClassNotFoundException になる。設定ファイルからクラス名を読む場合はタイポや classpath 不足への備えが必要
FAQ
フレームワーク内部や共通基盤では使われますが、業務ロジックで常用するのは避けるのが無難です。カプセル化を崩すとリファクタリング時に追従が難しくなります。
getDeclaredMethods はそのクラスで宣言されたメソッド(private 含む)を返します。getMethods は継承チェーン上の public メソッドをすべて返しますが、private は含みません。
起動オプションに --add-opens でモジュールとパッケージを指定します。ライブラリ側では InaccessibleObjectException を catch し、フォールバック処理を用意しておくと安全です。