Java Reflection

Java Reflectionの定義

リフレクション(Reflection)とは、オブジェクトを通じてクラスの情報を解析するプログラミング手法をいう。投影、反射という辞書的な意味も持つ。

Javaのリフレクションでは、クラス、インターフェース、メソッドを探したり、オブジェクトを生成したり、変数を変更したり、メソッドを呼び出したりできる。ReflectionはJava標準APIである。

Reflectionでは次のような情報を取得できる。この情報を取得して、オブジェクトを生成したり、メソッドを呼び出したり、変数の値を変更したりできる。

Class Package
Class java.lang
Constructor java.lang.reflect
Method java.lang.reflect
Field java.lang.reflect

リフレクションを理解するということ

Springのようなフレームワークは内部処理で多くのリフレクションを利用する。そのため、リフレクションを理解すると次のような利点がある。

  • フレームワークのソースコードを読んで理解できる。
  • ソースコードを読まなくても、フレームワーク内部の処理をイメージできるようになる。
  • 自分でフレームワークを作れるようになる。
    • 最近のJavaでは既存のOSS(Open Source Software)フレームワークを使用することが多いため、自分で作るケースはあまり多くないだろう。

ぜひリフレクションを理解して、初級から中級Javaエンジニアへステップアップしよう。

通常、ビジネスロジックなどでリフレクションを使用することは推奨されないため注意が必要である。理由は後で説明する。

リフレクションの基本

Classクラスを取得する

クラスを取得するにはClassクラスを使用する。Classクラスを使用する方法は次のとおりである。

方法1

Class<クラス名> オブジェクト名 = クラス名.class;

方法2

クラス名 オブジェクト名1 = new クラス名();
Class<? extends クラス名> オブジェクト名2 = オブジェクト名1.getClass();

方法3

Class<?> オブジェクト名1 = Class.forName("クラス名");

forNameメソッドを使用する場合は、ClassNotFoundExceptionの例外処理が必要である。

なお、リフレクションに関連する例外の共通クラスであるReflectiveOperationExceptionで処理することもできる。

インスタンス生成

インスタンスを生成する方法は次のとおりである。

// Java 9以前
Class<クラス名> オブジェクト名1 = クラス名.class;
Object オブジェクト名2 = オブジェクト名1.newInstance(); // @Deprecated(since="9")

// Java 9以降
Class<クラス名> オブジェクト名1 = クラス名.class;
Object オブジェクト名2 = オブジェクト名1.getDeclaredConstructor().newInstance()

これは次に相当する。

クラス名 オブジェクト名2 = new クラス名();

メソッドの取得と実行

メソッドを取得するには、MethodクラスのオブジェクトにgetMethodメソッドの戻り値を保存する。

Method オブジェクト名 = "Classクラスのオブジェクト名".getMethod("メソッド名", "引数1の型名.class", "引数2の型名.class", ...);

getMethodメソッドの第2引数以降は、Classクラスの可変引数である。

メソッドを実行するには、invokeメソッドを実行する。

"Methodクラスのオブジェクト".invoke("生成されたインスタンス", "引数1", "引数2", ...)

invokeメソッドの第2引数以降は可変引数になる。

インスタンス生成、メソッド取得、実行の例

上記の内容をサンプルプログラムで確認してみよう。

次のコードには単純なsetgetメソッドがある。

package com.devkuma.basic.reflection.ex1;

public class Foo {
    private String str;

    public void setStr(String str) {
        this.str = str;
    }

    public String getStr() {
        return str;
    }
}

次のコードはFooのオブジェクトを生成し、setStrgetStrメソッドを実行する。

package com.devkuma.basic.reflection.ex1;

import java.lang.reflect.Method;

public class Main {

    public static void main(String[] args) {
        try {
            // クラスを取得
            Class<?> fooClazz = Class.forName("com.devkuma.basic.reflection.ex1.Foo");
            // インスタンスを生成
            Object myObj = fooClazz.getDeclaredConstructor().newInstance();

            // メソッド(setStr)を取得
            Method setStrMethod = fooClazz.getMethod("setStr", String.class);
            // メソッド(setStr)を実行
            setStrMethod.invoke(myObj, "test");

            // メソッド(getStr)を取得
            Method getStrMethod = fooClazz.getMethod("getStr");
            // メソッド(getStr)を実行
            System.out.println(getStrMethod.invoke(myObj));

        } catch (ReflectiveOperationException e) {
            e.printStackTrace();
        }
    }
}

実行結果:

test

上の例では、クラス名やメソッド名の文字列をforNameメソッド、getMethodメソッド、invokeメソッドなどの引数に指定して実行している。

privateフィールドの参照と変更

ここでは、privateフィールドを参照、変更する方法について見ていく。

次のサンプルコードで確認しよう。

package com.devkuma.basic.reflection.ex1;

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

public class Main {

    public static void main(String[] args) {
        try {
            // クラスを取得
            Class<?> fooClazz = Class.forName("com.devkuma.basic.reflection.ex1.Foo");
            // インスタンスを生成
            Object myObj = fooClazz.getDeclaredConstructor().newInstance();

            // メソッド(setStr)を取得
            Method setStrMethod = fooClazz.getMethod("setStr", String.class);
            // メソッド(setStr)を実行
            setStrMethod.invoke(myObj, "test");

            // メソッド(getStr)を取得
            Method getStrMethod = fooClazz.getMethod("getStr");
            // メソッド(getStr)を実行
            System.out.println(getStrMethod.invoke(myObj));

            
            // フィールド(str)を取得
            Field strField = fooClazz.getDeclaredField("str");
            strField.setAccessible(true);
            System.out.println(strField.get(myObj));

            // フィールド(str)を変更
            strField.set(myObj, "test2");
            System.out.println(strField.get(myObj));

        } catch (ReflectiveOperationException e) {
            e.printStackTrace();
        }
    }
}

実行結果:

test
test2

このサンプルコードでは、まずsetStrメソッドでフィールドに値を保存する。

その値をFieldクラスのオブジェクトであるstrFieldとして取得し、getメソッドを使って取り出している。そして、その値をstrFieldオブジェクトからsetメソッドを使って変更している。

リフレクションのリスク

リフレクションを使用するときは、次の点に注意する必要がある。

クラス設計を破壊するリスク

ここまで述べたように、アクセス修飾子privateで「カプセル化」した値を直接取得、変更できた。
このような直接操作を可能にするため、リフレクションには既存のクラス設計を破壊してしまうリスクがある。

コンパイル時にエラーが検出されない

以前は、リフレクションを使用した部分のコードはコンパイル時のエラー検出対象外であり、エラーを検出できなかった。
リフレクション部分のエラーは、プログラム実行時にしか検出できなかった。
しかしJava 7からは、ReflectiveOperationExceptionでリフレクション関連例外の共通クラスが定義されたため、例外処理ができる。

まとめ

ここではリフレクションについて説明した。

リフレクションを使用すると、後からクラス名やメソッド名など、変数に代入する値を変更するだけで別のクラスやメソッドを実行できる。プログラムをテストする場合などに使うと便利である。