[Java] 29. Reflection機能を使う方法(Class編)


Study / Java    作成日付 : 2019/09/18 20:02:14   修正日付 : 2021/03/11 18:20:43

こんにちは。明月です。


この投稿はJavaのReflection機能を使う方法(Class編)に関する説明です。


Reflectionとはクラスの構造を分析して動的ローディングが可能にする機能です。ということに説明されていますが、この意味では何の意味が分からないですね。

link - https://www.oracle.com/technical-resources/articles/java/javareflection.html

今までJavaでインスタンスを生成することはnewキーワードを使って生成します。

link - [Java] 7. クラスを作成する方法(コンストラクタを作成方法)


でも、newキーワードを使わなくてインスタンスを生成する方法があります。

import java.lang.reflect.Constructor;
// ノードクラス
class Node {
  // コンストラクタ
  public Node() { }
  // 関数生成
  public void print() {
    // コンソール出力
    System.out.println("Hello world");
  }
}
public class Example {
  // 実行関数
  public static void main(String... args) {
    try {
      // Nodeクラスタイプを取得
      Class<?> cls = Node.class;
      // Nodeクラスのコンストラクタを取得する。
      Constructor<?> constructor = cls.getConstructor();
      // コンストラクタを通ってnewInstance関数を呼び出してNodeインスタンスを生成する。
      Node node = (Node)constructor.newInstance();
      // nodeインスタンスのprint関数を実行する。
      node.print();
    } catch (Throwable e) {
      e.printStackTrace();
    }
  }
}


結果をみればNodeクラスのprintを呼び出してコンソールでHello worldの結果が出力しました。

上の例では何処でもnew Nodeという実装はありません。Reflectionの機能を通ってNodeのインスタンスを生成したことです。


そうならnew Nodeを使ったら、もっと少ないステップでインスタンスを生成することができますが、なぜReflectionを使うことでしょうか?

import java.lang.reflect.Constructor;
// ノードクラス
class Node {
  // コンストラクタ
  public Node() { }
  // 関数を再定義した。
  @Override
  public String toString() {
    // コンソール出力
    System.out.println("Hello world");
    return null;
  }
}
public class Example {
  // 実行関数
  public static void main(String... args) {
    try {
      // Class.forNameの関数を使って文字列でClass<?>タイプを取得する。
      Class<?> clz = Class.forName("Node");
      // Stringで取得したクラスタイプでコンストラクタを取得する。
      Constructor<?> constructor = clz.getConstructor();
      // コンストラクタを通ってnewInstance関数を呼び出してNodeインスタンスを生成する。
      Object node =  constructor.newInstance();
      // nodeインスタンスのtoString関数を実行する。
      node.toString();
    } catch (Throwable e) {
      e.printStackTrace();
    }
  }
}


上の例をみればStringタイプでクラスを探索してインスタンス生成することができます。

その機能で二つ方法を実装することができます。

一つ目はStringの値はコンパイル段階でチェックしないです。上の例でtoStringを再定義することでキャストなしでtoStringでコンソール出力しました。

つまり、Exampleクラス内部ではNodeクラスを使ってないので、コンパイルする段階でNodeクラスがなくてもコンパイルできます。つまり、動的バインディングが可能するという意味です。

public class Example {
  // 実行関数
  public static void main(String... args) {
    try {
      // Class.forNameの関数を使って文字列でClass<?>タイプを取得する。
      Class<?> clz = Class.forName("Node");
      // Stringで取得したクラスタイプでコンストラクタを取得する。
      Constructor<?> constructor = clz.getConstructor();
      // コンストラクタを通ってnewInstance関数を呼び出してNodeインスタンスを生成する。
      Object node =  constructor.newInstance();
      // nodeインスタンスのtoString関数を実行する。
      node.toString();
    } catch (Throwable e) {
      e.printStackTrace();
    }
  }
}


上の例はExampleクラスだけ実装してNodeクラスは実装しませんでした。コンパイル段階は問題ないです。

でも、実行するとNodeクラスがないというエラーが発生しますね。


Nodeクラスを作ってコンパイルして同じフォルダに置きます。


今回はバインディングにして実行することを確認できます。この意味は起動中のプログラムの再実行なしてソース交換ができるという意味です。理論はそうです。

しかし、実際のサービスでは予想できないエラーが発生する可能性があるのでしない方がよいです。

例えば、Nodeインスタンスを生成してメモリに登録されました。でもソースが切り替えました。それだけみてもエラーが発生する可能性もあると思われますね。


二つ目はインスタンス生成を外部で可能です。

// ノードクラス
class Node1 {
  // コンストラクタ
  public Node1() { }
  // 関数を再定義した。
  @Override
  public String toString() {
    return "Node1";
  }
}
// ノードクラス
class Node2 {
  // コンストラクタ
  public Node2() { }
  // 関数を再定義した。
  @Override
  public String toString() {
    return "Node2";
  }
}
// ノードクラス
class Node3 {
  // コンストラクタ
  public Node3() { }
  // 関数を再定義した。
  @Override
  public String toString() {
    return "Node1";
  }
}
public class Example {
  // クラス取得関数
  private static Object getClass(String name) {
    // パラメータの値がNode1の場合Node1インスタンスを返却
    if("Node1".equals(name)) {
      return new Node1();
    // パラメータの値がNode2の場合Node2インスタンスを返却
    } else if("Node2".equals(name)) {
      return new Node2();
    // パラメータの値がNode3の場合Node3インスタンスを返却
    } else if("Node3".equals(name)) {
      return new Node3();
    }
    return null;
  }
  // 実行関数
  public static void main(String... args) {
    // インスタンスを受け取る
    Object instance = getClass("Node1");
    // コンソール出力
    System.out.println(instance.toString());
  }
}


上のソースをみればgetClass関数のパラメータにNode1の値を入れてNode1のインスタンスを受け取ります。実際によく実装する方法です。

でも、クラスの種類が多くなるとgetClass関数のif文は増えます。例えば100個になるとif elseだけ100個実装します。


この時にReflectionを利用すればソースが簡単になります。

// ノードクラス
class Node1 {
  // コンストラクタ
  public Node1() { }
  // 関数を再定義した。
  @Override
  public String toString() {
    return "Node1";
  }
}
// ノードクラス
class Node2 {
  // コンストラクタ
  public Node2() { }
  // 関数を再定義した。
  @Override
  public String toString() {
    return "Node2";
  }
}
// ノードクラス
class Node3 {
  // コンストラクタ
  public Node3() { }
  // 関数を再定義した。
  @Override
  public String toString() {
    return "Node1";
  }
}
public class Example {
  // クラス取得関数
  private static Object getClass(String name) {
    try {
      // Class.forNameの関数を使って文字列でClass<?>タイプを取得する。
      Class<?> clz = Class.forName(name);
      // Stringで取得したクラスタイプでコンストラクタを取得する。
      Constructor<?> constructor = clz.getConstructor();
      // コンストラクタを通ってnewInstance関数を呼び出してNodeインスタンスを生成する。
      return constructor.newInstance();
    } catch (Throwable e) {
      return null;
    }
  }
  // 実行関数
  public static void main(String... args) {
    // インスタンスを受け取る
    Object instance = getClass("Node1");
    // コンソール出力
    System.out.println(instance.toString());
  }
}


上のソースはClassが100個に増えてもgetClassの修正はないです。


でも、Reflectionは万能ではありません。問題は性能ですね。

import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.List;
// ノードクラス
class Node {
  // コンストラクタ
  public Node() { }
  // 関数を再定義した。
  @Override
  public String toString() {
    // コンソール出力
    System.out.println("Hello world");
    return null;
  }
}
public class Example {
  // クラス取得関数
  private static Object getClass(String name) {
    try {
      // Class.forNameの関数を使って文字列でClass<?>タイプを取得する。
      Class<?> clz = Class.forName(name);
      // Stringで取得したクラスタイプでコンストラクタを取得する。
      Constructor<?> constructor = clz.getConstructor();
      // コンストラクタを通ってnewInstance関数を呼び出してNodeインスタンスを生成する。
      return constructor.newInstance();
    } catch (Throwable e) {
      return null;
    }
  }
  // 実行関数
  public static void main(String... args) {
    // 例で100万個のインスタンスを生成する。
    int count = 1000000;
    // インスタンスを格納するリスト
    List<Object> list = new ArrayList<>(count);
    // 開始
    long startTime = System.currentTimeMillis();
    // ループ
    for (int i = 0; i < count; i++) {
      // 割りあってリストに格納
      list.add(new Node(Integer.toString(i)));
    }
    // 終了
    long endTime = System.currentTimeMillis();
    // 時間確認
    System.out.println(endTime - startTime);
    // リストクリア
    list.clear();
    // 開始
    startTime = System.currentTimeMillis();
    // ループ
    for (int i = 0; i < count; i++) {
      // 割りあってリストに格納
      list.add(getClass("Node", Integer.toString(i)));
    }
    // 終了
    endTime = System.currentTimeMillis();
    // 時間確認
    System.out.println(endTime - startTime);
  }
}


例で変数が一つあるクラスを100万個のインスタンスに生成しました。性能の差異が5倍になります。多分、変数が増えて生成するインスタンスが増えたらもっと遅くなると思います。

ここで差異が発生する理由はClass.forNameとgetConstructorの呼び出しせいです。クラスを探索する時間が係るからです。

そうならその探索の一回に設定して実装しましょう。

import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.List;
// ノードクラス
class Node {
  // コンストラクタ
  public Node() { }
  // 関数を再定義した。
  @Override
  public String toString() {
    // コンソール出力
    System.out.println("Hello world");
    return null;
  }
}
public class Example {
  // 実行関数
  public static void main(String... args) {
    // 例で100万個のインスタンスを生成する。
    int count = 1000000;
    // インスタンスを格納するリスト
    List<Object> list = new ArrayList<>(count);
    // 開始
    long startTime = System.currentTimeMillis();
    // ループ
    for (int i = 0; i < count; i++) {
      // 割りあってリストに格納
      list.add(new Node(Integer.toString(i)));
    }
    // 終了
    long endTime = System.currentTimeMillis();
    // 時間確認
    System.out.println(endTime - startTime);
    // リストクリア
    list.clear();
    // Class.forNameの関数を使って文字列でClass<?>タイプを取得する。
    Class<?> clz = Class.forName("Node");
    // Stringで取得したクラスタイプでコンストラクタを取得する。
    Constructor<?> constructor = clz.getConstructor(String.class);
    // 開始
    startTime = System.currentTimeMillis();
    // ループ
    for (int i = 0; i < count; i++) {
      // 割りあってリストに格納
      list.add(constructor.newInstance(Integer.toString(i)));	
    }
    // 終了
    endTime = System.currentTimeMillis();
    // 時間確認
    System.out.println(endTime - startTime);
  }
}


それでもReflectionの方が遅いですね。でも上の差異なら少し遅くても便利性がよいからいいと思います。でもClass<?> clzとConstructor<?> constructorのデータを何処かの変数に格納しなければならないです。

Reflectionの便利性はよいですが、結局にReflectionのクラスタイプやコンストラクタを管理するソースを実装することで簡単ではありません。

どっちがよいかいうと仕様によって選択することですね。

Reflectionはコンパイルエラーで取れないです。つまり、プログラムを実行しなければ、エラーを確認することができないです。Reflection機能が多くなるとテストが大変になるし、結局品実に問題がなる可能性があります。


Reflection機能は普通Unitテスト環境とFrameworkを構築する時、DI(依存性の注入)の時によく使う機能です。


ここまでJavaのReflection機能を使う方法(Class編)に関する説明でした。


ご不明なところや間違いところがあればコメントしてください。

最新投稿