🍍

8.2 リフレクションの仕組みとAPI~Java Advanced編

2023/11/05に公開

はじめに

自己紹介

皆さん、こんにちは、斉藤賢哉と申します。私はこれまで、25年以上に渡って企業システムの開発に携わってきました。特にアーキテクトとして、ミッションクリティカルなシステムの技術設計や、Javaフレームワーク開発などの豊富な経験を有しています。
様々なセミナーでの登壇や雑誌への技術記事寄稿の実績があり、また以下のような書籍も執筆しています。

いずれもJava EEJakarta EE)を中心にした企業システム開発のための書籍です。中でも 「アプリケーションアーキテクチャ設計パターン」は、(Javaに限定されない)比較的普遍的なテーマを扱っており、内容的にはまだまだ陳腐化していないため、興味のある方は是非手に取っていただけると幸いです(中級者向け)。

Udemy講座のご紹介

この記事の内容は、私が講師を務めるUdemy講座『Java Advanced編』の一部の範囲をカバーしたものです。『Java Advanced編』はこちらのリンクから購入できます(セールス対象外のためいつも同じ価格)。また定価の約30%OFFで購入可能なクーポンをZenn内で定期的に発行していますので、興味のある方は、ぜひ私の他の記事をチェックしてみてください。

この講座は、以下のような皆様にお薦めします。

  • Javaの基本的なスキルを習得済みで、さらなるレベルアップを目指している方
  • 将来的なキャリアとして、希少性の高い上級エンジニアやアーキテクトを志向している方
  • フリーランスエンジニアとして付加価値の更なる向上を図っている方
  • 「Oracle認定Javaプログラマ」の資格取得を目指している方

この記事を含むシリーズ全体像

この記事はJava SEの一部の機能・仕様を取り上げたものですが、一連のシリーズになっており、シリーズ全体でJava SEを網羅しています。また認定資格である「Oracle認定Javaプログラマ」(Silver、Gold)の範囲もカバーしています。シリーズの全体像および「Oracle認定Javaプログラマ」の範囲との対応関係については、以下を参照ください。

https://zenn.dev/kenya_saitoh/articles/3fe26f51ab001b

8.2 リフレクションの仕組みとAPI

チャプターの概要

このチャプターでは、クラスのメタ情報を扱うことにより様々な処理を可能にするリフレクションという機能について学びます。
リフレクションは一種の「黒魔術」であり、主にフレームワークの開発などで多用されています。

8.2.1 リフレクションの概要

リフレクションとは

リフレクションとは、クラスのメタ情報、すなわちフィールド、メソッド、およびコンストラクタに関する情報をもとに、様々な処理を行うことができる機能です。
リフレクションを使うと、文字列として与えられたクラス名やメソッド名などの情報から、クラスを生成したり、メソッドを呼び出したりすることができます。
例えばMainクラスというクラスにおいて、文字列"Foo"と"sayHello"から、FooクラスのsayHello()メソッドを呼び出すことができます。必要なのはあくまでも文字列なので、Mainクラスをコンパイルするにあたり、Fooクラスは必要ありません。
どのようなときに、こういった機能が必要になるのでしょうか。通常、クラスのメンバーにアクセスするためには、そのクラスがクラスパスからロードされ、ソースコード上に見える形でコンパイルされる必要があります。例えば開発者がMainクラスを作成するとき、その中からFooクラスのsayHello()メソッドを呼び出す場合は、Fooクラスが存在しないとコンパイルはとおりません。
ところがフレームワークのように汎用性の高いソフトウェアを開発する場合、呼び出し対象はFooクラスかもしれませんし、Barクラスかもしれませんが、それは実行時まで決定することができない場合があります。つまり呼び出し対象になるクラスが、コンパイル時には必ずしも存在しているとは限らない、というケースがありえるのです。そのような場合、リフレクションを使うと、呼び出し先のクラスやメソッドを、実行時に動的に決定することができるようになります。
リフレクションは、通常のJavaアプリケーション開発で利用するケースは少ないかもしれませんが、特にフレームワークなどの開発では多用される機能です。

【図8-2-1】リフレクションの用途
image.png

リフレクションのためのClassクラスのAPI

Classクラスはクラスのメタ情報を管理するクラスですが、リフレクションのための様々なAPIが定義されています。
その中から特に主要なものを以下に示します。

API(メソッド) 説明
static Class<?> forName(String) 指定されたFQCNを持つクラス(またはインタフェース)のClassオブジェクトを返す。このとき、クラスパスから当該クラスがメモリにロードされ、スタティック初期化しが呼び出される。当該クラスが見つからない場合は、ClassNotFoundExceptionを送出する。
Class getSuperclass() このClassオブジェクトの親となるクラス(またはインタフェース)を表すClassオブジェクトを返す。
Class[] getInterfaces() このClassオブジェクトが実装したインタフェースをClassオブジェクトの配列で返す。
Method getMethod(String, Class<?>[]) このClassオブジェクトが持つメソッドの中から、第一引数の名前で、第二引数に指定されたClassオブジェクトの配列(可変引数)を引数に持つpublicメソッドを抽出し、そのMethodオブジェクトを返す。当該メソッドが見つからない場合は、NoSuchMethodExceptionを送出する。
Method getDeclaredMethod(String, Class<?>[]) getMethod()メソッドと同様だが、publicメソッドではなく、すべてのメソッドが対象になる。
Method[] getMethods() このClassオブジェクトが表すクラス(またはインタフェース)の、すべてのpublicメソッド(継承メソッドを含む)を表すMethodオブジェクトの配列を返す。
Method[] getDeclaredMethods() getMethods()メソッドと同様だが、対象はpublicだけではなくすべてのメソッド(ただし継承メソッド含まない)になる。
Field getField(String) このClassオブジェクトが持つフィールドの中から、指定された名前を持つpublicフィールドを抽出し、そのFieldオブジェクトを返す。当該メソッドが見つからない場合は、NoSuchFieldExceptionを送出する。
Field getDeclaredField(String) getField()メソッドと同様だが、publicフィールドだけではなく、すべてのフィールドが対象になる。
Field[] getFields() このClassオブジェクトが表すクラス(またはインタフェース)の、すべてのpublicフィールド(継承されたフィールド含む)を表すFieldオブジェクトの配列を返す。
Field[] getDeclaredFields() getFields()メソッドと同様だが、対象はpublicだけではなくすべてのフィールド(ただし継承フィールド含まない)になる。
Constructor<T> getConstructor(Class<?>[]) このClassオブジェクトが持つコンストラクタの中から、指定されたClassオブジェクトの配列(可変引数)を引数に持つpublicコンストラクタを抽出し、そのConstructorオブジェクトを返す。当該コンストラクタが見つからない場合は、NoSuchMethodExceptionを送出する。
Constructor<T> getDeclaredConstructor(Class<?>[]) getConstructor()メソッドと同様だが、publicコンストラクタだけではなく、すべてのコンストラクタが対象になる。
Constructor<?>[] getConstructors() このClassオブジェクトが表すクラス(またはインタフェース)の、すべてのpublicコンストラクタを表すConstructorオブジェクトの配列を返す。
Constructor<?>[] getDeclaredConstructors() getConstructors()メソッドと同様だが、publicコンストラクタだけではなく、すべてのコンストラクタが対象になる。

リフレクションのためのその他クラスとAPI

ClassクラスのAPIにおいて、戻り値として定義されてるMethodクラス、Fieldクラス、Constructorクラスは、リフレクションのためのクラスです。それらも含めて、リフレクションのためのクラスには、以下のようなものがあります。いずれもjava.lang.reflectパッケージに所属しています。

  • メソッド … java.lang.reflect.Methodクラス
  • フィールド … java.lang.reflect.Fieldクラス
  • コンストラクタ … java.lang.reflect.Constructorクラス
  • メソッド/コンストラクタ引数 … java.lang.reflect.Parameterクラス

これらのクラスにも数多くのAPIが定義されていますが、すべては紹介しきれないため、ここでは代表的なものを取り上げます。
まずはMethodクラスのAPIです。

API(メソッド) 説明
String getName() このMethodオブジェクトによって表されるメソッドの名前を返す。
Object invoke(Object, Object...) このMethodオブジェクトによって表されるメソッドを、第一引数のインスタンスに対して、第二引数のObject配列(可変引数)をパラメータとして呼び出す。
void setAccessible(boolean) このMethodオブジェクトの可視性を、指定されたboolean値に設定する。
Parameter[] getParameters() このMethodオブジェクトによって表されるメソッド引数を、Parameterオブジェクトの配列で返す。
Class<?>[] getParameterTypes() このMethodオブジェクトによって表されるメソッド引数の型を、宣言された順にClassオブジェクトの配列で返す。
Class<?> getReturnType() このMethodオブジェクトによって表されるメソッド戻り値の型を、Classオブジェクトで返す。

次にFieldクラスのAPIです。

API(メソッド) 説明
String getName() このFieldオブジェクトによって表されるフィールドの名前を返す。
Object get(Object) 指定されたインスタンスについて、このFieldオブジェクトによって表されるフィールドの値を返す。
void set(Object, Object) 第一引数のインスタンスの、このFieldオブジェクトによって表されるフィールドの値を、第二引数の値に設定する。
Class<?> getType() このFieldオブジェクトの型を表すClassオブジェクトを返す。

続いてConstructorクラスのAPIです。

API(メソッド) 説明
String getName() このConstructorオブジェクトによって表されるコンストラクタの名前を返す。
T newInstance(Object...) このConstructorオブジェクトによって表されるコンストラクタを、指定されたObject配列(可変引数)をパラメータとして呼び出す。
Parameter[] getParameters() このMethodオブジェクトによって表されるメソッド引数を、Parameterオブジェクトの配列で返す。
Class<?>[] getParameterTypes() このConstructorオブジェクトによって表されるコンストラクタ引数の型を、宣言された順にClassオブジェクトの配列で返す。

MethodクラスまたはConstructorクラスのgetParameters()メソッドで取得されるParameterクラスは、メソッドやコンストラクタの引数を表すものです。
Parameterクラスには、以下のようなAPIがあります。

API(メソッド) 説明
String getName() このParameterオブジェクトによって表されるパラメータの名前を返す。
Class<?> getType() このParameterオブジェクトの型を表すClassオブジェクトを返す。

これらのAPIを使った具体的な処理は、次のレッスンで説明します。

8.2.2 リフレクションの具体的な活用方法

クラスの動的な生成とメソッド呼び出し

このレッスンでは、リフレクションの対象として、以下のGreetingクラス(パッケージは"pro.kensait.java.advanced.lsn_8_2_2")を使います。

pro.kensait.java.advanced.lsn_8_2_2.Greeting
public class Greeting {
    public String getYes() {
        return "Yes!";
    }
    private String getNo() {
        return "No!";
    }
    public String getHello(String name, int age) {
        return "Hello! 私は" + name + "、" + age + "歳です。";
    }
}

それでは文字列として与えられるFQCNから、Greetingクラスのインスタンスを生成し、メソッドの呼び出しを行っていきましょう。
以下は、リフレクションによってGreetingクラスのgetYes()メソッドを呼び出し、その結果を受け取るためのコードです。

snippet (pro.kensait.java.advanced.lsn_8_2_2.Main_MethodInvocation_1)
Class<?> clazz = Class.forName(
         "pro.kensait.java.advanced.lsn_8_2_2.Greeting"); //【1】
Constructor<?> constructor = clazz.getDeclaredConstructor(); //【2】
Object target = constructor.newInstance(); //【3】
Method method = clazz.getMethod("getYes"); //【4】
Object result = method.invoke(target); //【5】
System.out.println(result);

まずは、リフレクションの起点となるClassオブジェクトの取得が必要です。文字列情報からClassオブジェクトを取得するためには、ClassクラスのforName()メソッドを使用します【1】。このメソッド呼び出しにより、Greetingクラスがロードされます。
次にこのClassオブジェクトから、インスタンスを生成するためのConstructorオブジェクトを取得するために、getDeclaredConstructor()メソッドを呼び出します【2】。getDeclaredConstructor()メソッドは、引数にClassオブジェクトの配列を渡すことで、その型の順番によってオーバーロードされたコンストラクタを特定します。この例では、引数を持たないデフォルトコンストラクタが取得対象のため、引数なしで呼び出します。
このようにしてConstructorオブジェクトを取得したら、newInstance()メソッドによりインスタンスを生成します【3】。コンストラクタが引数を持つ場合はnewInstance()メソッドに引数を指定しますが、ここではデフォルトコンストラクタが対象のため、引数なしで呼び出します。
次にメソッドを呼び出す準備として、getMethod()メソッドにメソッド名"getYes"を指定して、Methodオブジェクトを取得します【4】。getMethod()メソッドは、第二引数以降にClassオブジェクトの配列を渡すことで、その型の順番によってメソッドのシグネチャを特定します。この例では、引数を持たないメソッドが取得対象のため、第二引数なしで呼び出します。
続いてメソッド呼び出しです。メソッドを呼び出すためには、取得したMethodオブジェクトのinvoke()メソッドに、生成したインスタンスを渡します【5】。このようにすると対象のメソッド(ここではgetYes()メソッド)が呼び出され、その結果がObject型で返されます。

privateメソッドの呼び出し

既出の例はpublicなメソッドを呼び出していましたが、privateなメソッドでもリフレクションで呼び出し可能です。そのためには前項のgetMethod()メソッド呼び出しを、以下のように修正します。

snippet (pro.kensait.java.advanced.lsn_8_2_2.Main_MethodInvocation_2)
Method method = clazz.getDeclaredMethod("getNo");
method.setAccessible(true);

getDeclaredMethod()メソッドは、アクセス修飾子に関わらず、そのクラスで宣言されたすべてのメソッドを対象にすることができます。ただし取得したgetNo()メソッドはprivateメソッドのため、メソッド呼び出しを実行する前に、MethodオブジェクトのsetAccessible()メソッドにtrueを渡すことで、アクセス可能にする必要があります。

引数のあるメソッドの呼び出し

既出の例では、引数のないメソッド呼び出しを行っていましたが、ここではString型とint型という2つの引数を取る、getHello()メソッドを呼び出し対象にします。そのためには、前項のコードにおけるMethodオブジェクト取得【4】と、メソッド呼び出し【5】のコードを、以下のように修正します。

snippet (pro.kensait.java.advanced.lsn_8_2_2.Main_MethodInvocation_3)
Method method = clazz.getMethod("getHello",
        String.class, Integer.TYPE); //【1】
Object[] params = {"Alice", 25}; //【2】
Object result = method.invoke(target, params); //【3】

getMethod()メソッドによって引数を持つメソッドを取得する場合、第二引数以降に引数の型を表すClassオブジェクトの配列(可変引数)を指定する必要があります【1】。このコードでは第二引数にString型、第三引数にint型を指定していますが、このようにすることでgetHello(String, int)というメソッドのシグネチャを特定します。なおプリミティブ型をClassオブジェクトとして表す場合は、当該ラッパークラスのTYPEフィールドを指定します。次にメソッド呼び出し時に引数として渡す値を、Object配列として生成します【2】。
続いてメソッド呼び出しです。引数のあるメソッドを呼び出すためには、取得したMethodオブジェクトのinvoke()メソッドに、生成したインスタンスと、引数としてのObject配列を渡します【3】。このようにすると対象のメソッド(ここではgetHello()メソッド)が呼び出され、その結果である文字列"Hello! 私はAlice、25歳です。"が、Object型で返されます。

リフレクションの高度な活用例

リフレクションは一種の「黒魔術」であり、上手に利用すると、高度で汎用的な処理を比較的少ないコードで実現することができます。ここではその一例として、クラスからクラスへの値の詰め替え処理を取り上げます。例えば「人物」を表すPersonクラスがあり、氏名(personName)、年齢(age)、性別(gender)、住所(address)といったフィールドを持つものとします。また「顧客」を表すCustomerクラスがあり、ID(id)、氏名(customerName)、性別(gender)、住所(address)といったフィールドを持つものとします。
ここで「もし氏名が一致していたら、PersonからCustomerへ、同じフィールド名があった場合に値を詰め替える」という処理を、リフレクションによって実現してみましょう。

【図8-2-2】リフレクションの活用例
image.png

まずは以下のようにして、それぞれのクラスのインスタンスと、Classオブジェクトを取得します。

snippet (pro.kensait.java.advanced.lsn_8_2_2.Main_InstanceCopy)
// Person、Customer、それぞれのインスタンスを生成する
Person p = new Person("Alice", 25, GenderType.FEMALE, "中央区1-1-1");
Customer c = new Customer(1, "Alice");
// Person、Customer、それぞれのClassオブジェクトを取得する
Class<Person> personClazz = Person.class;
Class<Customer> customerClazz = Customer.class;

次に、以下のようにして「もし氏名が一致していたら」の判定を行います。

snippet (pro.kensait.java.advanced.lsn_8_2_2.Main_InstanceCopy)
Field personNameField = personClazz.getDeclaredField("personName"); //【1】
Field customerNameField = customerClazz.getDeclaredField("customerName"); //【2】
personNameField.setAccessible(true); //【3】
customerNameField.setAccessible(true); //【4】
if (! Objects.equals(personNameField.get(p), customerNameField.get(c))) { //【5】
    return;
}

PersonインスタンスのpersonNameフィールドの値を取得し【1】、同じようにCusotmerインスタンスのcustomerNameフィールドの値を取得します【2】。取得したらそれぞれのフィールドの可視性をtrueに書き換えます【3、4】。そしてそれぞれのフィールド値をFieldクラスのget()メソッドで取得し、氏名が不一致の場合は、処理を抜けるものとします【5】。
さて、この判定処理の結果、先進めすることになった場合は、以下のようにして「PersonからCustomerへ、同じフィールド名があった場合に値を詰め替える」という処理を行います。

snippet (pro.kensait.java.advanced.lsn_8_2_2.Main_InstanceCopy)
PERSON : for (Field personField : personClazz.getDeclaredFields()) {
    for (Field customerField : customerClazz.getDeclaredFields()) {
        if (personField.getName().equals(customerField.getName())) { //【1】
            personField.setAccessible(true); //【2】
            customerField.setAccessible(true); //【3】
            customerField.set(c, personField.get(p)); //【4】
            continue PERSON;
        }
    }
}

このコードでは、Personクラスのフィールド群と、Customerクラスのフィールド群、それぞれを配列として取り出し、二重ループで処理を行います。ループ処理の中では、それぞれのフィールド名をFieldクラスのgetName()メソッドで取得し、フィールド名の等価性を判定します【1】。フィールド名が同じだった場合は、それぞれのフィールドの可視性をtrueに書き換えます【2、3】。そしてPerson側のフィールド値をFieldクラスのget()メソッドで取得し、その値をCusotmer側のフィールドのset()メソッドに渡すことで、当該フィールド値を上書きします【4】。

さて、このコードを実行すると、PersonからCustomerへの値の詰め替えが行われ、結果としてCustomerインスタンスは、以下のような値を持つようになります。

Customer [id=1, name=Alice, gender=FEMALE, address=中央区1-1-1]

性別(gender)と住所(address)が、Personインスタンスからコピーされていることが分かります。

なおこのコードでは、単一のPersonインスタンスから単一のCustomerインスタンスへの値の詰め替えを行いましたが、実際のアプリケーションでは、それぞれがリストに格納されているようなケースが想定されます。そのような場合は、この一連の処理のさらに外側で、リストから値を取り出しながら、ループ処理によって値の詰め替えを行うことになるでしょう。
またこのコードでは、Personクラス、Customerクラスといった具合にクラスを「決め打ち」しましたが、2つのClassオブジェクトを引数に取るメソッドを用意すれば、クラスの型に関わらず、フィールドの名前が同じ場合にその値を詰め替える、という汎用的な処理を実現することも可能です。

このチャプターで学んだこと

このチャプターでは、以下のことを学びました。

  1. リフレクションとは、クラスのメタ情報をもとに様々な処理を行う機能であること。
  2. リフレクションによって、コンパイル時に存在しないクラスを文字列から動的に呼び出すことが可能であること。
  3. リフレクションのための様々なAPIについて。
  4. リフレクションによるクラスの動的な生成方法や、メソッド呼び出しの方法について。
  5. リフレクションを活用することで、高度で汎用的な処理を少ないコードで実現可能であること。

Discussion