Unknown Region

プログラムでハマったエラーとその解決方法についての備忘録メモ

【Java】親に持つGenerics(総称型)<T>のClassを取得する

備忘録がてらに、こちらへメモ。

 

以下の様なクラス構成に於ける「Generics(総称型)<T>」のクラスの取得方法について。

Hoge.java

/**
 * Hogeクラス
 */
public class Hoge {
}

BaseController.java

/**
 * ベースクラスでGenerics(総称型<T>)を持つ
 */
public abstract class BaseController<T> {
}

HogeController.java

/**
 * BaseControllerを継承した子クラスで、Hogeを扱う
 */
public class HogeController extends BaseContainer<Hoge> {
}

ここでやりたいことは、BaseController内からGenerics(総称型)<T>が何のクラスなのかを突き止めたいというもの。

特にインターネット上では「Generics(総称型)<T>のインスタンスを作るにはどうしたら良いか?」という質問をチラホラ見かけます。

Javaでは「new T()」はできないというのが常識ですが、裏技的な方法でクラスを取得することは可能です。

クラスさえ取得できれば、リフレクションを駆使してインスタンスを作ることもできるかもしれません。

 

さてさて本題に入ります。

Generics(総称型)<T>」のクラス取得処理を実現する方法は2つあります。

  1. 親のコンストラクタに「Tの可変長引数」を使用する
  2. リフレクションでGenerics(総称型)そのものを取得する

 

 

1. 親のコンストラクタに「Tの可変長引数」を使用する

可変長引数は「メソッドの引数の数を指定せず、任意に取る様にできる」機能です。

引数は[(クラス)... 変数]の形で指定し、メソッド内でその変数は「指定したクラスの配列」として利用することができます。

以下の例ではコンストラクタにこれを利用し「引数を0個与える」ことで「Generics(総称型)<T>の配列」を取得しています。

配列からは「それが何のクラスの配列なのか」を取得できるので、「Generics(総称型)<T>」の正体のクラスを取得することができます。

可変長引数を利用した例: BaseController.java

@SuppressWarnings("unchecked")
public abstract class BaseController<T> {

  // Tのクラスを格納するフィールド
  public Class<T> genericsClazz = null;
  
  /**
   * コンストラクタ
   * 
   * @param args
   */
  public BaseController(T... args) {
    this.genericsClazz = (Class<T>) args.getClass().getComponentType();
  }
}

 

この状態でHogeControllerのインスタンスを作成し、genericsClazzを取得すると以下の様になります。

テスト実行プログラム: Demo.java

HogeController hogeController = new HogeController();
System.out.println("genericsClazz: " + hogeController.genericsClazz.getName()); // genericsClazz: Hoge

ただしこの方法は、場合によっては使用できないため注意が必要です。

特に「コンストラクタが複数ある場合で、別のコンストラクタからこのコンストラクタを呼んだ場合」、取得できるのは「Hoge」ではなく「Object」になってしまいます。

 

可変長引数の利用が上手く行かない例: BaseController.java

@SuppressWarnings("unchecked")
public abstract class BaseController<T> {

  // Tのクラスを格納するフィールド
  public Class<T> genericsClazz = null;

  /**
   * コンストラクタ
   * 
   * @param name
   */
  public BaseController(String name) {
    this();
  }
  
  /**
   * コンストラクタ
   * 
   * @param args
   */
  public BaseController(T... args) {
    this.genericsClazz = (Class<T>) args.getClass().getComponentType();
  }
}

うまく行かない例: HogeController.java

public class HogeController extends BaseContainer<Hoge> {
  /**
   * コンストラクタ
   * 
   * @param name
   */
  public HogeController(String name) {
    super(name);
  }
}

テスト実行プログラム: Demo.java

HogeController hogeController = new HogeController("piyo");
System.out.println("genericsClazz: " + hogeController.genericsClazz.getName()); // genericsClazz: java.lang.Object

ただ以下のパターンでは取得できたので、同じクラス内での別のコンストラクタ呼び出しをする場合だけNGなのだろうか……?

うまく行かなさそうでうまく行く例: HogeController.java

/**
 * BaseControllerを継承した子クラス
 */
public class HogeController extends BaseController {
  public HogeController(String name) {
	  super();
  }
}

 

2. リフレクションでGenerics(総称型)そのものを取得する

こちらについては解説を省きますが、1に比べると使用できる状況に制限はありません。

ただリフレクションなので実行速度が気になるところ。

Java8以降で使用できるStreamAPIを利用していますが、別に利用しない用に書き換えることは可能です。

 

リフレクションを利用した例: BaseController.java

@SuppressWarnings("unchecked")
public abstract class BaseController<T> {
  public Class genericsClazz = Stream.iterate((Object) this.getClass(), clazz -> (Object) ((Class<?>) clazz).getSuperclass())
          .map(Objects::requireNonNull)
          .filter(Class.class::isInstance)
          .map(Class.class::cast)
          .map(clazz -> clazz.getGenericSuperclass())
          .filter(ParameterizedType.class::isInstance)
          .map(ParameterizedType.class::cast)
          .map(parameterizedType -> parameterizedType.getActualTypeArguments()[0])
          .filter(Class.class::isInstance)
          .map(Class.class::cast)
          .findFirst()
          .get();
}

テスト実行プログラム: Demo.java

HogeController hogeController = new HogeController();
System.out.println("genericsClazz: " + hogeController.genericsClazz.getName()); // genericsClazz: Hoge

 

探せば他にももっと良い方法があるのかもしれません。

久々にこんな長文を書いた気がする