Javaのリフレクションとは、クラス名・フィールド名・メソッド名を「文字列」で指定して、実行時にクラスの情報を調べたり、フィールドを読み書きしたり、メソッドを呼び出したりする仕組みです。 Class オブジェクトを取得し、getDeclaredField や getDeclaredMethod でメンバーを取り出し、setAccessible(true) のあと get / set / invoke を呼ぶ、という3段階で使います。

この記事では、すべてのコードを OpenJDK 21.0.10 で実行し、出力とエラー文をそのまま掲載しています。Class取得の3通りから、private の操作、InaccessibleObjectException と --add-opens、record、MethodHandle との速度比較まで、コピーしてそのまま動かせる形でまとめました。Java 25・26 での変化は公式ドキュメントで確認して注記しています。

Javaリフレクションの早見表

やりたいこと 書き方 ポイント
Classを取得(型が決まっている) String.class コンパイル時に確定。最も手軽
Classを取得(オブジェクトから) obj.getClass() 実行時の実際の型が返る
Classを取得(名前の文字列から) Class.forName("java.lang.String") 完全修飾名。見つからないと ClassNotFoundException
publicフィールドを取得 clazz.getField("rank") 親クラスの public も対象
privateフィールドを取得 clazz.getDeclaredField("name") そのクラスで宣言したものだけ
privateに触れるようにする field.setAccessible(true) get / set / invoke の前に呼ぶ
フィールドを読む・書く field.get(obj) / field.set(obj, 値) static は obj に null
publicメソッドを取得 clazz.getMethod("add", int.class, int.class) 引数の型を Class で並べる
privateメソッドを取得 clazz.getDeclaredMethod("label", String.class) setAccessible(true) が必要
メソッドを呼ぶ method.invoke(obj, 引数...) static は第1引数に null。戻り値は Object
インスタンスを生成 clazz.getDeclaredConstructor().newInstance() 引数ありは型を指定して取得
recordの項目を取得 clazz.getRecordComponents() 宣言順に返る
JDK内部のprivateに触れる 起動時に --add-opens java.base/java.lang=ALL-UNNAMED 付けないと InaccessibleObjectException
呼び出し先の例外を取り出す e.getCause() InvocationTargetException に包まれて届く

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Javaリフレクションの使い方|getDeclaredField・invoke」

リフレクションでできることと使いどころ

リフレクションは「コンパイル時には相手のクラスがわからない」ときに力を発揮します。 名前を文字列で渡すため、設定ファイルやアノテーションの内容に応じて呼び出し先を切り替えられます。

DIコンテナ、JSON ライブラリ、テストフレームワークなどが、この仕組みで成り立っています。

以降のコードは、ファイル名を クラス名.java で保存して java クラス名.java と実行すれば、そのまま動きます(Java 11 以降の単一ファイル実行)。

Classオブジェクトを取得する3通り

リフレクションの入口は Class オブジェクトです。取得方法は「クラス名.class」「getClass()」「Class.forName()」の3通りで、どれで取得しても同じクラスなら同じオブジェクトが返ります。

public class ClassGet {
    public static void main(String[] args) throws Exception {
        // 1. クラス名.class(コンパイル時に型が決まっているとき)
        Class<String> c1 = String.class;
        // 2. インスタンス.getClass()(手元にオブジェクトがあるとき)
        Class<?> c2 = "abc".getClass();
        // 3. Class.forName("完全修飾名")(名前が文字列で渡されるとき)
        Class<?> c3 = Class.forName("java.lang.String");

        System.out.println(c1.getName());
        System.out.println(c2.getSimpleName());
        System.out.println(c3.getPackageName());
        System.out.println(c1 == c2 && c2 == c3);

        Object list = new java.util.ArrayList<String>();
        System.out.println(list.getClass().getName());
        try {
            Class.forName("com.example.NoSuchClass");
        } catch (ClassNotFoundException e) {
            System.out.println(e);
        }
    }
}

実行結果(java ClassGet.java、OpenJDK 21.0.10):

java.lang.String
String
java.lang
true
java.util.ArrayList
java.lang.ClassNotFoundException: com.example.NoSuchClass

4行目の true が示すとおり、3つの方法で得た Class は同一のオブジェクトです。5行目のように、変数の型が Object でも getClass() は実際の型を返します。クラス名が設定ファイルや引数で渡されるときは Class.forName を使います。Class.forName にはパッケージ名を含めた完全修飾名を渡します("String" ではなく "java.lang.String")。ネストしたクラスは "CtorDemo$Item" のように $ でつなぎます。

privateフィールドを読む・書き換える(getDeclaredField)

private フィールドは getDeclaredField で取得し、setAccessible(true) を呼んでから get / set で操作します。 例では、アクセス権の違いがそのまま表れるよう、対象クラス Member を別ファイルにしています。

public class Member {
    private String name = "佐藤";
    private int point = 100;
    public String rank = "silver";
}
import java.lang.reflect.Field;

public class FieldDemo {
    public static void main(String[] args) throws Exception {
        Member m = new Member();
        Class<?> clazz = m.getClass();

        // publicフィールドは getField でも取れる
        Field rank = clazz.getField("rank");
        System.out.println("rank = " + rank.get(m));

        // privateフィールドは getDeclaredField + setAccessible(true)
        Field name = clazz.getDeclaredField("name");
        name.setAccessible(true);
        System.out.println("name = " + name.get(m));

        // 値を書き換える
        name.set(m, "鈴木");
        System.out.println("name = " + name.get(m));

        // プリミティブ型は getInt / setInt も使える
        Field point = clazz.getDeclaredField("point");
        point.setAccessible(true);
        point.setInt(m, point.getInt(m) + 50);
        System.out.println("point = " + point.getInt(m));
        System.out.println("型 = " + point.getType() + ", 名前 = " + point.getName());

        // privateを getField で探すと見つからない
        try {
            clazz.getField("name");
        } catch (NoSuchFieldException e) {
            System.out.println(e);
        }
    }
}

実行結果(javac Member.java FieldDemo.java → java FieldDemo):

rank = silver
name = 佐藤
name = 鈴木
point = 150
型 = int, 名前 = point
java.lang.NoSuchFieldException: name

要点は次の2つです。

  • getField は public だけ、getDeclaredField は private も含む。private を getField で探すと NoSuchFieldException になります(最終行)。
  • get の戻り値は Object です。int などのプリミティブは getInt / setInt を使うとキャストを省けます。

setAccessibleを省くとIllegalAccessExceptionになる

別のクラスの private メンバーを setAccessible(true) なしで操作すると、IllegalAccessException が発生します。 上と同じ Member クラスで確かめます。

import java.lang.reflect.Field;

public class NoAccess {
    public static void main(String[] args) throws Exception {
        Member m = new Member();
        Field f = Member.class.getDeclaredField("name");
        try {
            System.out.println(f.get(m));          // setAccessible を呼んでいない
        } catch (IllegalAccessException e) {
            System.out.println(e);
        }
        f.setAccessible(true);
        System.out.println(f.get(m));
    }
}

実行結果(javac Member.java NoAccess.java → java NoAccess):

java.lang.IllegalAccessException: class NoAccess cannot access a member of class Member with modifiers "private"
佐藤

なお、同じファイル内のネストしたクラス(static class など)同士は、setAccessible なしでも private に届きます。Java 11 から、入れ子の関係にあるクラスは互いの private にアクセスできる仲間(nestmate)として扱われるためです。main と同じファイルの static class に private フィールドを置き、setAccessible なしで f.get(...) を呼ぶコードは、Java 21.0.10 でそのまま値を表示しました。

クラスを別ファイルに分けたら IllegalAccessException になったというときは、この違いが理由です。別クラスの private を扱うコードには、常に setAccessible(true) を書いておくと安定します。

メソッドを呼び出す(invoke)

メソッドは getMethod(名前, 引数の型...) で取得し、invoke(対象オブジェクト, 引数...) で呼び出します。 同じ名前のメソッドが複数ある(オーバーロード)ことがあるため、引数の型を Class で指定して1つに絞ります。

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class MethodDemo {
    static class Calc {
        public int add(int a, int b) { return a + b; }
        private String label(String s, int n) { return s + "-" + n; }
        public static String version() { return "1.0"; }
        public int divide(int a, int b) { return a / b; }
        public void log(String... msgs) { System.out.println("log: " + String.join(",", msgs)); }
    }

    public static void main(String[] args) throws Exception {
        Calc calc = new Calc();
        Class<?> clazz = Calc.class;

        // publicメソッド:引数の型を Class で指定して取得
        Method add = clazz.getMethod("add", int.class, int.class);
        Object result = add.invoke(calc, 3, 4);
        System.out.println("add = " + result);

        // privateメソッド:getDeclaredMethod + setAccessible(true)
        Method label = clazz.getDeclaredMethod("label", String.class, int.class);
        label.setAccessible(true);
        System.out.println("label = " + label.invoke(calc, "item", 7));

        // staticメソッド:第1引数は null
        Method version = clazz.getMethod("version");
        System.out.println("version = " + version.invoke(null));

        // 可変長引数:配列を Object にキャストして1つの引数として渡す
        Method log = clazz.getMethod("log", String[].class);
        log.invoke(calc, (Object) new String[] {"a", "b"});

        // 呼び出し先の例外は InvocationTargetException に包まれる
        Method divide = clazz.getMethod("divide", int.class, int.class);
        try {
            divide.invoke(calc, 1, 0);
        } catch (InvocationTargetException e) {
            System.out.println("包んだ例外 = " + e);
            System.out.println("本来の例外 = " + e.getCause());
        }

        // int と Integer は別の型として扱われる
        try {
            clazz.getMethod("add", Integer.class, Integer.class);
        } catch (NoSuchMethodException e) {
            System.out.println(e);
        }
    }
}

実行結果(java MethodDemo.java、OpenJDK 21.0.10):

add = 7
label = item-7
version = 1.0
log: a,b
包んだ例外 = java.lang.reflect.InvocationTargetException
本来の例外 = java.lang.ArithmeticException: / by zero
java.lang.NoSuchMethodException: MethodDemo$Calc.add(java.lang.Integer,java.lang.Integer)

つまずきやすい点を表にまとめます。

場面 起きること こうするとよい
引数が int のメソッドを Integer.class で探す NoSuchMethodException int.class と Integer.class は別物。宣言どおりの型を渡す
呼び出し先で例外が発生 InvocationTargetException に包まれる e.getCause() で本来の例外を取り出す
可変長引数のメソッド 配列がばらばらの引数として展開される (Object) new String[]{...} とキャストして1つの引数にする

InvocationTargetException のメッセージは null になることが多いので、ログに出すときは e.getCause() を記録すると原因がすぐにわかります。

コンストラクタからインスタンスを生成する

インスタンスの生成は getDeclaredConstructor(引数の型...).newInstance(引数...) です。 以前よく使われた clazz.newInstance() は Java 9 から非推奨となっているため、引数なしでも getDeclaredConstructor().newInstance() と書きます。

import java.lang.reflect.Constructor;

public class CtorDemo {
    static class Item {
        private final String name;
        private final int stock;
        public Item() { this("未設定", 0); }
        public Item(String name, int stock) { this.name = name; this.stock = stock; }
        private Item(String name) { this(name, -1); }
        @Override public String toString() { return "Item[" + name + ", " + stock + "]"; }
    }

    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("CtorDemo$Item");

        // 引数なしコンストラクタ
        Object a = clazz.getDeclaredConstructor().newInstance();
        System.out.println(a);

        // 引数ありコンストラクタ
        Constructor<?> c2 = clazz.getConstructor(String.class, int.class);
        Object b = c2.newInstance("ノート", 12);
        System.out.println(b);

        // privateコンストラクタ
        Constructor<?> c3 = clazz.getDeclaredConstructor(String.class);
        c3.setAccessible(true);
        System.out.println(c3.newInstance("ペン"));

        // Class<Item> から取得すると、戻り値が Item 型になる
        Item item = Item.class.getConstructor(String.class, int.class).newInstance("付箋", 30);
        System.out.println(item);
    }
}

実行結果(java CtorDemo.java、OpenJDK 21.0.10):

Item[未設定, 0]
Item[ノート, 12]
Item[ペン, -1]
Item[付箋, 30]

Item.class.getConstructor(...) のように型のわかっている Class<Item> から取得すると、newInstance の戻り値が Item 型になり、キャストが不要です。Class.forName で得た Class<?> の場合は Object で受け取り、(Runnable) のようにインターフェースへキャストして使うのが定番の形です。

getXxxとgetDeclaredXxxの違い(メソッド早見表)

getXxx は「public だけ・親クラスの分も含む」、getDeclaredXxx は「アクセス修飾子を問わない・そのクラスで宣言した分だけ」です。

メソッド 対象のアクセス修飾子 親クラス・インターフェースの分 複数まとめて取得
getField(名前) public のみ 含む getFields()
getDeclaredField(名前) すべて(private も) 含まない getDeclaredFields()
getMethod(名前, 型...) public のみ 含む(Object 由来も) getMethods()
getDeclaredMethod(名前, 型...) すべて 含まない getDeclaredMethods()
getConstructor(型...) public のみ コンストラクタは継承されない getConstructors()
getDeclaredConstructor(型...) すべて 同上 getDeclaredConstructors()

親子のクラスで実際に確かめます。

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;

public class DeclaredDiff {
    static class Parent {
        public String parentPublic;
        protected String parentProtected;
        private String parentPrivate;
        public void parentPublicMethod() {}
        private void parentPrivateMethod() {}
    }
    static class Child extends Parent {
        public String childPublic;
        String childPackage;
        private String childPrivate;
        public void childPublicMethod() {}
        private void childPrivateMethod() {}
    }

    public static void main(String[] args) {
        Class<?> c = Child.class;
        System.out.println("getFields         : " + names(c.getFields()));
        System.out.println("getDeclaredFields : " + names(c.getDeclaredFields()));
        System.out.println("getMethods        : " + Arrays.stream(c.getMethods())
                .filter(m -> m.getDeclaringClass() != Object.class).map(Method::getName).sorted().toList());
        System.out.println("getDeclaredMethods: " + Arrays.stream(c.getDeclaredMethods())
                .map(Method::getName).sorted().toList());

        // 親クラスの private まで集めるには getSuperclass() でたどる
        for (Class<?> k = c; k != null && k != Object.class; k = k.getSuperclass()) {
            System.out.println(k.getSimpleName() + " -> " + names(k.getDeclaredFields()));
        }
    }
    static java.util.List<String> names(Field[] fs) {
        return Arrays.stream(fs).map(Field::getName).sorted().toList();
    }
}

実行結果(java DeclaredDiff.java、OpenJDK 21.0.10):

getFields         : [childPublic, parentPublic]
getDeclaredFields : [childPackage, childPrivate, childPublic]
getMethods        : [childPublicMethod, parentPublicMethod]
getDeclaredMethods: [childPrivateMethod, childPublicMethod]
Child -> [childPackage, childPrivate, childPublic]
Parent -> [parentPrivate, parentProtected, parentPublic]
  • getFields には親の parentPublic が入り、getDeclaredFields には入りません。
  • 親クラスの private フィールドまで集めたいときは、getSuperclass() で親をたどりながら getDeclaredFields() を呼びます(最後の2行)。

InaccessibleObjectExceptionの再現と--add-opens

Java 17 以降、JDK 内部のクラス(java.lang.String など)の private メンバーに setAccessible(true) を呼ぶと、InaccessibleObjectException が発生します。 これはモジュールシステム(Java 9)の強いカプセル化が、Java 17 の JEP 403 で既定になったためです。String の内部フィールド value で再現します。

import java.lang.reflect.Field;

public class OpenDemo {
    public static void main(String[] args) throws Exception {
        Field value = String.class.getDeclaredField("value");
        System.out.println("取得はできる: " + value);
        value.setAccessible(true);   // ここで例外
        byte[] bytes = (byte[]) value.get("abc");
        System.out.println("長さ = " + bytes.length);
    }
}

実行結果(java OpenDemo.java、OpenJDK 21.0.10。スタックトレースは一部省略):

取得はできる: private final byte[] java.lang.String.value
Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make field private final byte[] java.lang.String.value accessible: module java.base does not "opens java.lang" to unnamed module @226a82c4
    at java.base/java.lang.reflect.Field.setAccessible(Field.java:177)
    at OpenDemo.main(OpenDemo.java:7)

getDeclaredField までは成功し、setAccessible(true) の行で例外になるのがポイントです。メッセージは「相手のモジュール java.base が、パッケージ java.lang を、自分の側(クラスパスで動かすと unnamed module)に開いていない」という意味です。

メッセージに出てきた名前をそのまま並べて、--add-opens モジュール/パッケージ=ALL-UNNAMED を起動オプションに付けると、そのパッケージが開かれます。

java --add-opens java.base/java.lang=ALL-UNNAMED OpenDemo.java

実行結果(OpenJDK 21.0.10):

取得はできる: private final byte[] java.lang.String.value
長さ = 3

テスト実行で発生するときは、ビルドツールのテスト用 JVM 引数に同じオプションを渡します。利用中のライブラリが原因のときは、まず新しい版への更新を試すと、オプションなしで解消することが多くあります。

finalフィールドの書き換えはどうなるか

リフレクションでの final フィールドの書き換えは、対象によって結果が異なります。 Java 21.0.10 で、通常クラスの final・static final・record の3種類を試しました。

import java.lang.reflect.Field;

public class FinalDemo {
    static class Config {
        private final String mode = new String("prod");
        static final Integer LIMIT = Integer.valueOf(10);
    }
    record Point(int x, int y) {}

    public static void main(String[] args) throws Exception {
        // 1. 通常クラスの final インスタンスフィールド
        Config c = new Config();
        Field mode = Config.class.getDeclaredField("mode");
        mode.setAccessible(true);
        mode.set(c, "dev");
        System.out.println("final インスタンス: " + mode.get(c));

        // 2. static final フィールド
        Field limit = Config.class.getDeclaredField("LIMIT");
        limit.setAccessible(true);
        try {
            limit.set(null, 99);
        } catch (IllegalAccessException e) {
            System.out.println("static final: " + e.getMessage());
        }

        // 3. record のフィールド
        Point p = new Point(1, 2);
        Field x = Point.class.getDeclaredField("x");
        x.setAccessible(true);
        try {
            x.set(p, 100);
        } catch (IllegalAccessException e) {
            System.out.println("record 書き込み: " + e.getMessage());
        }
    }
}

実行結果(java FinalDemo.java、OpenJDK 21.0.10):

final インスタンス: dev
static final: Can not set static final java.lang.Integer field FinalDemo$Config.LIMIT to java.lang.Integer
record 書き込み: Can not set final int field FinalDemo$Point.x to java.lang.Integer

通常クラスの final インスタンスフィールドは書き換えられ、static final と record のフィールドは IllegalAccessException になりました(読み取りはどれも可能です)。

Java 26 では、通常クラスの final フィールドを書き換えたときに警告が出るようになりました(JEP 500「Prepare to Make Final Mean Final」)。Oracle の JDK 26 移行ガイドと inside.java の解説によると、起動オプション --illegal-final-field-mutation の既定値が warn(ほかに allow・debug・deny)で、将来は deny が既定になる予定です。特定のモジュールに書き換えを認める --enable-final-field-mutation もあります。ここは Java 21 では動かせないため、公式ドキュメントで確認した内容です。Java 25 までは、上の実行結果のとおりに動きます。

final フィールドは「読むだけ」にして、値を変えたいときは新しいインスタンスを作ると、どの版でも同じように動きます。

recordのコンポーネントを取得する

record の項目は getRecordComponents() で宣言順に取得でき、getAccessor() で値を読むメソッドが手に入ります。 フィールドに直接触れずに public なアクセサを呼ぶため、setAccessible は不要です。

import java.lang.reflect.Constructor;
import java.lang.reflect.RecordComponent;
import java.util.Arrays;

public class RecordDemo {
    record Book(String title, int price, java.util.List<String> tags) {}

    public static void main(String[] args) throws Exception {
        Book book = new Book("Java入門", 2800, java.util.List.of("java", "入門"));
        Class<?> clazz = book.getClass();
        System.out.println("isRecord = " + clazz.isRecord());

        // コンポーネントは宣言順に返る
        for (RecordComponent rc : clazz.getRecordComponents()) {
            Object value = rc.getAccessor().invoke(book);
            System.out.println(rc.getName() + " : " + rc.getGenericType() + " = " + value);
        }

        // コンポーネントの型から正規コンストラクタを取得して生成
        Class<?>[] types = Arrays.stream(clazz.getRecordComponents())
                .map(RecordComponent::getType).toArray(Class<?>[]::new);
        Constructor<?> canonical = clazz.getDeclaredConstructor(types);
        Object copy = canonical.newInstance("Java応用", 3200, java.util.List.of());
        System.out.println(copy);

        // record 以外では null
        System.out.println(String.class.getRecordComponents());
    }
}

実行結果(java RecordDemo.java、OpenJDK 21.0.10):

isRecord = true
title : class java.lang.String = Java入門
price : int = 2800
tags : java.util.List<java.lang.String> = [java, 入門]
Book[title=Java応用, price=3200, tags=[]]
null

getDeclaredFields() の順序は仕様で保証されていませんが、getRecordComponents() は宣言順と決まっています。 そのため、コンポーネントの型を並べれば正規コンストラクタ(全項目を受け取るコンストラクタ)を確実に取得できます。

MethodHandleとの違いと速度計測

MethodHandle は、Java 7 で加わった java.lang.invoke パッケージの仕組みで、メソッドやフィールドへの「型つきの参照」です。 リフレクションが呼び出しのたびにアクセス権を確認するのに対し、MethodHandle は取得時(Lookup 時)に一度だけ確認します。

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.invoke.VarHandle;

public class HandleDemo {
    static class Member {
        private String name = "佐藤";
        private String greet(String who) { return who + "さん、こんにちは。" + name + "です"; }
    }

    public static void main(String[] args) throws Throwable {
        // 同じクラス(ネスト)内なので lookup() で private に届く
        MethodHandles.Lookup lookup = MethodHandles.lookup();

        // staticメソッド:Integer.parseInt(String)
        MethodHandle parse = lookup.findStatic(Integer.class, "parseInt",
                MethodType.methodType(int.class, String.class));
        System.out.println((int) parse.invokeExact("42") + 1);

        // privateメソッド・privateフィールド:privateLookupIn で対象クラス用の Lookup を得る
        MethodHandles.Lookup priv = MethodHandles.privateLookupIn(Member.class, lookup);
        MethodHandle greet = priv.findVirtual(Member.class, "greet",
                MethodType.methodType(String.class, String.class));
        Member m = new Member();
        System.out.println((String) greet.invoke(m, "田中"));

        VarHandle name = priv.findVarHandle(Member.class, "name", String.class);
        name.set(m, "鈴木");
        System.out.println((String) name.get(m));

        // 例外は包まれず、そのまま届く
        try {
            int n = (int) parse.invokeExact("abc");
        } catch (NumberFormatException e) {
            System.out.println(e);
        }
    }
}

実行結果(java HandleDemo.java、OpenJDK 21.0.10):

43
田中さん、こんにちは。佐藤です
鈴木
java.lang.NumberFormatException: For input string: "abc"
比較項目 リフレクション(Method) MethodHandle
取得 clazz.getMethod("add", int.class) lookup.findVirtual(クラス, "add", MethodType)
アクセス権の確認 呼び出しのたび 取得時に1回
private への到達 setAccessible(true) MethodHandles.privateLookupIn(クラス, lookup)
引数と戻り値 Object(ボクシングあり) invokeExact なら型がそのまま通る
呼び出し先の例外 InvocationTargetException に包まれる そのまま届く
メンバーの一覧取得 できる(getDeclaredMethods など) できない(名前と型を知っている前提)
フィールド操作 Field.get / set VarHandle(Java 9 以降)

invokeExact は引数と戻り値の型を完全に一致させる必要があります(戻り値の (int) キャストもそのためです)。自動で変換してほしいときは invoke を使います。

2,000万回ずつ呼んで速度を比べる

同じメソッドを、直接呼び出し・Method.invoke・static final に持った MethodHandle で 2,000 万回ずつ呼び、1回あたりの時間を測りました。「呼ぶたびに getMethod で探す」場合も加えています。

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Method;

public class Bench {
    public static class Counter {
        private int n;
        public int add(int x) { n = n * 31 + x; return n; }
    }
    static final int LOOP = 20_000_000;
    static final MethodHandle MH;
    static {
        try {
            MH = MethodHandles.lookup().findVirtual(Counter.class, "add",
                    MethodType.methodType(int.class, int.class));
        } catch (ReflectiveOperationException e) { throw new ExceptionInInitializerError(e); }
    }

    static int direct(Counter c) {
        int r = 0;
        for (int i = 0; i < LOOP; i++) r ^= c.add(i);
        return r;
    }
    static int reflect(Counter c, Method m) throws Exception {
        int r = 0;
        for (int i = 0; i < LOOP; i++) r ^= (Integer) m.invoke(c, i);
        return r;
    }
    static int handle(Counter c) throws Throwable {
        int r = 0;
        for (int i = 0; i < LOOP; i++) r ^= (int) MH.invokeExact(c, i);
        return r;
    }
    static int lookupEveryTime(Counter c) throws Exception {
        int r = 0;
        for (int i = 0; i < LOOP / 20; i++)
            r ^= (Integer) Counter.class.getMethod("add", int.class).invoke(c, i);
        return r;
    }

    public static void main(String[] args) throws Throwable {
        Method method = Counter.class.getMethod("add", int.class);
        int sink = 0;
        for (int round = 1; round <= 5; round++) {   // 前半はウォームアップ
            long t0 = System.nanoTime(); sink ^= direct(new Counter());
            long t1 = System.nanoTime(); sink ^= reflect(new Counter(), method);
            long t2 = System.nanoTime(); sink ^= handle(new Counter());
            long t3 = System.nanoTime(); sink ^= lookupEveryTime(new Counter());
            long t4 = System.nanoTime();
            System.out.printf("round %d: 直接 %.2f / Method.invoke %.2f / MethodHandle %.2f / 毎回getMethod %.2f (ns/回)%n",
                    round, (t1 - t0) / (double) LOOP, (t2 - t1) / (double) LOOP,
                    (t3 - t2) / (double) LOOP, (t4 - t3) / (double) (LOOP / 20));
        }
        System.out.println("sink = " + sink);
    }
}

実行結果(java Bench.java、OpenJDK 21.0.10、2コアの Linux コンテナ):

round 1: 直接 2.99 / Method.invoke 23.43 / MethodHandle 2.81 / 毎回getMethod 119.72 (ns/回)
round 2: 直接 1.05 / Method.invoke 10.44 / MethodHandle 2.29 / 毎回getMethod 95.16 (ns/回)
round 3: 直接 1.10 / Method.invoke 12.11 / MethodHandle 1.01 / 毎回getMethod 51.13 (ns/回)
round 4: 直接 1.02 / Method.invoke 8.70 / MethodHandle 0.98 / 毎回getMethod 48.95 (ns/回)
round 5: 直接 0.93 / Method.invoke 9.41 / MethodHandle 1.00 / 毎回getMethod 50.78 (ns/回)
sink = 2006646688

3回実行して、ウォームアップ後(round 3〜5)の傾向は共通でした。簡易計測のため、数値は目安です。

呼び出し方 1回あたり(round 3〜5、3回の実行の範囲) 読み取れること
直接呼び出し 約 1〜2 ns 基準
static final の MethodHandle 約 1 ns 直接呼び出しとほぼ同じ
Method.invoke(Method を使い回す) 約 8〜12 ns 直接の8〜10倍ほど
毎回 getMethod してから invoke 約 49〜65 ns 探す処理が最も重い

Method や Field は一度取得したら保持して使い回すのが第一です。探す処理(getMethod)を毎回行うと、呼び出しそのものの数倍の時間がかかります。繰り返し大量に呼ぶ箇所では、MethodHandle を static final フィールドに持つと、JIT コンパイラが定数として最適化でき、直接呼び出しに近い速さになります。1回 10 ns 前後なら1万回呼んでも 0.1 ミリ秒ほどなので、起動時に数十回呼ぶ程度であれば Method.invoke で十分です。厳密に比べたいときは JMH(Java の計測用ツール)を使います。

リフレクションで出会う例外の早見表

例外 発生する場面 対処
ClassNotFoundException Class.forName の名前が見つからない 完全修飾名・クラスパスを確認。ネストは $
NoSuchFieldException フィールド名の誤り、private を getField で検索 getDeclaredField に変える。親クラスの分は親の Class から
NoSuchMethodException メソッド名・引数の型が不一致 int.class と Integer.class を区別。引数なしコンストラクタの有無を確認
IllegalAccessException setAccessible(true) を呼んでいない。static final・record への書き込み setAccessible(true) を足す。final は読むだけにする
InaccessibleObjectException JDK 内部や、開かれていないモジュールへの setAccessible --add-opens、またはライブラリ更新
InvocationTargetException 呼び出し先のメソッドが例外を投げた getCause() で本来の例外を取り出す

ClassNotFoundException・NoSuchFieldException・NoSuchMethodException・IllegalAccessException・InvocationTargetException・InstantiationException は、まとめて親の ReflectiveOperationException で受け取れます。

リフレクションを使うかどうかの判断表

リフレクションは便利な一方、名前の打ち間違いがコンパイル時に見つからず、実行して初めてわかるという性質があります。

やりたいこと まず検討する書き方 リフレクションが向く場合
種類ごとに処理を切り替える インターフェースと実装クラス、switch 式 クラス名が設定で渡され、事前に列挙できない
メソッドを値として渡す ラムダ式・メソッド参照(Member::getName) メソッド名が実行時に決まる
大量に繰り返し呼ぶ 直接呼び出し static final の MethodHandle

よくある質問

Q. getDeclaredFieldで親クラスのprivateフィールドが取れません。どうすればよいですか? A. getDeclaredField は、その Class で宣言したフィールドだけが対象です。clazz.getSuperclass().getDeclaredField("名前") のように、親クラスの Class から取得します。階層が深いときは、getSuperclass() が null(または Object.class)になるまでループでたどります。

Q. リフレクションは遅いと聞きました。使ってもよいですか? A. Java 21.0.10 での計測では、Method を使い回した invoke が1回あたり約 8〜12 ns、毎回 getMethod で探すと約 49〜65 ns でした。数回の呼び出しであれば気にならない大きさです。大量に繰り返す箇所は、Method を保持する、static final の MethodHandle にする、の順で検討します。

Q. Java 25やJava 26でコードの書き換えは必要ですか? A. この記事の API(getDeclaredField・invoke・getRecordComponents など)は、そのまま使えます。変化があるのは final フィールドの書き換えで、Java 26 では警告が表示されるようになりました(JEP 500、公式ドキュメントで確認)。

まとめ

  • リフレクションは 「Class を取得 → メンバーを取得 → setAccessible → 操作」 の流れで使う
  • Class の取得は クラス名.class・getClass()・Class.forName() の3通り。どれも同じオブジェクトが返る
  • getXxx は public のみで親の分も含む、getDeclaredXxx は private も含むがそのクラスの分だけ
  • private の操作には setAccessible(true)。省くと IllegalAccessException、JDK 内部が相手だと InaccessibleObjectException で、--add-opens が必要
  • 呼び出し先の例外は InvocationTargetException の getCause() に入っている
  • record は getRecordComponents() で宣言順に取得でき、アクセサ経由で値を読める
  • 速度は 「探す処理」が最も重い。Method は使い回し、繰り返しの多い箇所は static final の MethodHandle を検討する

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Javaリフレクションの使い方|getDeclaredField・invoke」

株式会社RJCでは、Javaのリフレクションのような言語の仕組みを仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?