入門Javaのリフレクション
はじめに
リフレクションとは
リフレクションとは、クラス・コンストラクタ・フィールド・メソッドなどを扱うための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
クラスがあるとします。
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
など、@...
で始まるアレです。
アノテーションは自分で作ることも可能です。
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"なので「要素」が正しいと思います。
今回はname
とvalue
という要素を定義しています。
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
を付加します。
@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