入門Javaのリフレクション

2024/08/10に公開

はじめに

リフレクションとは

リフレクションとは、クラス・コンストラクタ・フィールド・メソッドなどを扱うためのJava標準APIです。

具体的には、次のようなクラスがあります。

パッケージ クラス名 表すもの
java.lang Class クラス
java.lang.reflect Constructor コンストラクタ
java.lang.reflect Field フィールド
java.lang.reflect Method メソッド

リフレクションを知る意義

Springなどのフレームワークは、多くの内部処理でリフレクションを利用しています。なのでリフレクションを知ると、次のようなメリットがあります。

  • フレームワークのソースコードを読んで理解できるようになる!
  • ソースコードを読まずとも、フレームワーク内部の処理がイメージできるようになる!
  • 自分でフレームワークを作れるようになる!
    • 近年のJavaでは既存のOSSフレームワークを使うことが多いので、あんまりやらないかもですが

ぜひリフレクションを知って、初級を卒業して中級Javaエンジニアにステップアップしましょう!

一方、普段のお仕事で書く業務ロジックなどでリフレクションを使うことはおすすめしませんので、注意してください(理由は後述します)。

リフレクションの基本

Classクラス

Classクラスは、クラスを表すクラスです。リフレクションの中心的存在です。

Classクラスの定義はClass<T>のようにジェネリクス付きになっています。Tには表しているクラスが指定されます。

全てのクラスには、classというstaticフィールドのようなもの(後述)が暗黙的に作成されています。このフィールドのようなものには、そのクラスを表すClassインスタンスが代入されています。

つまり、こんなフィールドがあるイメージです。

これはイメージコードです
public class Hoge {
    public static final Class<Hoge> class = (Hogeを表すClassインスタンス);
}

.classは正確には「クラスリテラル」と呼ばれるリテラルで、フィールドではありません(参考URL)。ただし、前述の説明ではイメージ重視で「フィールドのようなもの」として説明しています。

これに加えて、全てのクラスにはgetClass()メソッドが定義されています(java.lang.Objectクラスで定義されています)。このメソッドの戻り値は、.classと同じ「そのクラスを表すClassインスタンス」です。

Hoge hoge = new Hoge();
Class<Hoge> clazz = hoge.getClass();

クラス名を取得する

Class クラスには、クラス名を取得するためのメソッドがいくつか用意されています。

メソッド名 説明
getName() 完全修飾クラス名(FQCN)を返す
getSimpleName() 単純クラス名を返す
Class<?> clazz = Object.class;
String fqcn = clazz.getName();
System.out.println(fqcn);
String name = clazz.getSimpleName();
System.out.println(name);
実行結果
java.lang.Object
Object

リフレクションでインスタンスを生成する

例えば、こんな Sample クラスがあるとします。

Sample.java
package com.example;

public class Sample {

    private int value;

    public Sample(int value) {
        this.value = value;
    }

    public int multi(int value2) {
        return value * value2;
    }

    @Override
    public String toString() {
        return "Sample{" +
                "value=" + value +
                '}';
    }
}

まず、Class.forName()メソッドでSampleクラスを表すClassインスタンスを生成します。引数にはSampleクラスの完全修飾名を指定します。

// Sampleクラスを表すClassインスタンスを生成する
Class<?> sampleClass = Class.forName("com.example.Sample");

次に、getConstructor()メソッドでSampleクラスのコンストラクタを取得します。引数には、欲しいコンストラクタの引数の型を指定します。

// Sampleクラスのコンストラクタを取得する
Constructor<?> constructor = sampleClass.getConstructor(int.class);

intなどの基本データ型もクラスリテラル(.class)を利用できます。

次に、取得したコンストラクタのnewInstance()メソッドでSampleインスタンスを生成します。引数には、コンストラクタに渡す引数を指定します。

// コンストラクタを利用してSampleインスタンスを生成する
Object sampleInstance = constructor.newInstance(100);

本当にSampleインスタンスが生成されたのか確認してみましょう。

// インスタンスの状態を表示
System.out.println(sampleInstance);

確かに、valueフィールドの値が100に設定されたSampleインスタンスであることが分かります。

実行結果
Sample{value=100}

フィールドを扱う

Classクラスには、フィールドを取得するメソッドがいくつかあります。

  • Field getField(String name)
  • Field getDeclaredField(String name)

どちらもnameで指定された名前のフィールドを取得するメソッドです。違いは次のとおりです。

メソッド名 publicフィールドの取得 public以外のフィールドの取得 スーパークラスで定義されたフィールドの取得
getField() ×
getDeclaredField() ×

getFields()getDeclaredFields()のようなFieldの配列を返すメソッドもあります。

今回はvalueフィールドがprivateなので、後者を利用します。

// valueフィールドを取得
Field valueField = sampleClass.getDeclaredField("value");

setAccesible()メソッドにtrueを指定すると、privateフィールドでも値を取得したり、値を代入したりできるようになります。

// valueフィールドの値を取得
Object value = valueField.get(sampleInstance);
// valueフィールドに値200を設定
valueField.set(sampleInstance, 200);

メソッドを扱う

Classクラスには、メソッドを取得するメソッドがいくつかあります。

  • Method getMethod(String name, Class<?>... parameterTypes)
  • Method getDeclaredMethod(String name, Class<?>... parameterTypes)

どちらもnameで指定された名前のメソッドを取得するメソッドです。違いは次のとおりです。

メソッド名 publicメソッドの取得 public以外のメソッドの取得 スーパークラスで定義されたメソッドの取得
getMethod() ×
getDeclaredMethod() ×

getMethods()getDeclaredMethods()のようなMethodの配列を返すメソッドもあります。

今回は前者を利用します。

// multiメソッドの取得
Method multiMethod = sampleClass.getMethod("multi", int.class);

取得したメソッドを実行するときはinvoke()メソッドに、メソッドを実行したいインスタンスと、引数に渡す値を指定します。

// multiメソッドの実行
Object ret = multiMethod.invoke(sampleInstance, 2);  // 100 * 2 = 200が戻り値となる

Fieldクラス同様、MethodクラスにもsetAccessible()メソッドがあります。

アノテーションの利用

アノテーションの自作

アノテーションとは、Java標準の@OverrideやSpringの@RequestMappingなど、@...で始まるアレです。

アノテーションは自分で作ることも可能です。

MyAnnotation.java
package com.example;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

// 実行時にもアノテーション情報を残す
@Retention(RetentionPolicy.RUNTIME)
// このアノテーションを付けられる箇所をクラス・インタフェースに指定する
@Target({ElementType.TYPE})
// このアノテーションを付けたことがJavadocにも掲載される
@Documented
public @interface MyAnnotation {
    String name() default "default";
    int value();
}

アノテーションを作成する場合は、classではなく@interfaceとして宣言します。

アノテーションの要素

MyAnnotationに定義されたメソッドのようなものは要素です。

よく「属性」とも呼ばれますが、英語では"element"なので「要素」が正しいと思います。

今回はnamevalueという要素を定義しています。

name要素には、defaultキーワードを利用してデフォルト値を設定しています。

@Retention

@Retentionは、@MyAnnotationの情報がクラスファイルや実行時に保持されるかどうかを指定します。

RetentionPolicy 説明
RUNTIME アノテーションはクラスファイルに記録され、かつ実行時もJVMによって保持されます。
CLASS アノテーションはクラスファイルに記録される一方、実行時はJVMによって保持されません。
SOURCE アノテーションはクラスファイルに記録されません。

@Retentionを付けなかった場合、CLASSと同じ扱いになります。

@Target

@Targetは、@MyAnnotationがメソッド・フィールドなどどこに付加できるかElementType列挙型でを指定します。

主なElementType 説明
CONSTRUCTOR コンストラクタ
FIELD フィールド
METHOD メソッド
TYPE クラス、インタフェースなど型の宣言

全てのElementTypeを確認したい場合はJavadocをご覧ください。

@Targetを付けなかった場合、型パラメータ以外の全箇所に付加可能になります。

@Documented

@Documentedを付加すると、@MyAnnotationを付加したクラスのJavadocに「このクラスは@MyAnnotationが付加されている」旨が掲載されます。

リフレクションでアノテーションを取得する

今回はSampleクラスに@MyAnnotationを付加します。

Sample.java
@MyAnnotation(999)
public class Sample {

}

名前がvalueの要素のみを指定する場合は、@MyAnnotation(999)と書くことができます(@MyAnnotation(value = 999)と書いてもOK)

クラスに付加されたアノテーションを取得する場合は、ClassクラスのgetAnnotation()メソッドを利用します。引数には取得したいアノテーションの型を指定します。

フィールドやメソッドに付加されたアノテーションを取得するには、Fieldクラス・MethodクラスのgetAnnotation()メソッドを利用します。

Class<?> sampleClass = Class.forName("com.example.Sample");
// クラスに付いたアノテーションを取得
MyAnnotation myAnnotation = sampleClass.getAnnotation(MyAnnotation.class);

アノテーションの要素値は次のように取得します。

// アノテーションの要素値を取得
String name = myAnnotation.name();  // "default"が取得できる
int value = myAnnotation.value();  // 999が取得できる

プロキシ

プロキシとは

プロキシとは、インタフェース実装クラスおよびそのインスタンスを実行時に作成する技術、およびその作成されたインスタンスを指す言葉です。

具体的には、java.lang.reflectパッケージにProxyというクラスがあります。

プロキシの作成

今回は、こんなインタフェースのプロキシを作成してみます。

package com.example;

public interface Command {
    int execute1(String command);

    int execute2(String command);
}

このインタフェースを実装したクラス&そのインスタンスは、Proxy.newProxyInstance()メソッドで作成します。

Command c = (Command) Proxy.newProxyInstance(
          Command.class.getClassLoader(),
          new Class<?>[]{ Command.class },
          new CommandInvocationHandler());

第1引数はクラスローダーです。クラスローダーとは、クラスファイルを読み込む役割を担ったものです。インタフェース名.class.getClassLoader()で良いでしょう。

第2引数には、プロキシが実装すべきインタフェースを配列で指定します。

第3引数は、プロキシ内の処理を記述するInvocationHandler実装クラスです(後述)。

プロキシ内の処理を記述する

プロキシの各メソッドが実行された際の処理は、InvocationHandlerインタフェースを実装して記述します。

package com.example;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Objects;

public class CommandInvocationHandler implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (Objects.equals("execute1", method.getName())) {
            // execute1()の処理
            if ("UP".equals(args[0])) {
                return 1;
            } else if ("DOWN".equals(args[0])) {
                return 0;
            } else {
                throw new IllegalArgumentException("Arg must be 'UP' or 'DOWN'");
            }
        }
        if (Objects.equals("execute2", method.getName())) {
            // execute2()の処理
            if ("LEFT".equals(args[0])) {
                return 2;
            } else if ("RIGHT".equals(args[0])) {
                return 3;
            } else {
                throw new IllegalArgumentException("Arg must be 'LEFT' or 'RIGHT'");
            }
        }
        throw new IllegalArgumentException("Invalid method");
    }
}

プロキシの呼び出し

作成されたプロキシのクラス名はどうなっているのか、表示して確認してみましょう。

// クラス名を表示
System.out.println(c.getClass().getName());

こんな風にcom.sun.proxy.$ProxyNとなっていたら、プロキシが作られている証拠です。

実行結果
com.sun.proxy.$Proxy0

メソッドを呼び出すと、InvocationHandler実装クラスに記述した処理が実行されます。

// execute1()の呼び出し
int ret1 = c.execute1("UP");
System.out.println(ret1);

// execute2()の呼び出し
int ret2 = c.execute2("RIGHT");
System.out.println(ret2);

// execute1()の呼び出し(例外発生)
int ret3 = c.execute1("LEFT");
System.out.println(ret3);
実行結果
1
3
Exception in thread "main" java.lang.IllegalArgumentException: Arg must be 'UP' or 'DOWN'
	at com.example.CommandInvocationHandler.invoke(CommandInvocationHandler.java:16)
	at com.sun.proxy.$Proxy0.execute1(Unknown Source)
	at com.example.ProxySample.main(ProxySample.java:19)

リフレクションの使いどころ

前にも書きましたが、リフレクションは基本的にフレームワークやライブラリを作るための技術です。

業務ロジックで使ってしまうと、クラス名を文字列で書くことになるため、後から保守する際に、そのクラスの利用箇所を見つけるのが非常に難しくなります。さらにsetAccessible()を利用してprivateフィールドに値を代入したりすると、せっかくカプセル化したものが無駄になってしまいます。

繰り返しになりますが、業務ロジックにはリフレクションを使わないでください。

Discussion