読者です 読者をやめる 読者になる 読者になる

JavaのGenericsのめんどいところ?

こんなシチュエーションのときJavaのGenericsはめんどくさい。もしかしたらもっとうまいやり方があるのかもしれないけど分からなかった。

とりあえず例を挙げてみる。

interface X {}
class Y implements X {}
class Z implements X {}

こんな感じでインターフェースとそれを実装したクラスがあるとします。

class Klass {
  private ArrayList<X> xlist;

  /**
   * X のリストから指定したクラスのインスタンスだけ取り出したリストを返す。
   */
  public List<X, T extends X> getElementsByClass(Class<T> c) {
    ArrayList<T> list = new ArrayList<T>();
    Iterator<X> it = xlist.iterator();
    while(it.hasNext()) {
      X x = it.next();
      if(c.isInstance(x))
        list.add(c.cast(x));
    }
    return list;
  }
}

そして,Xのリストを保持してるオブジェクトがあって,その中からXを継承したクラスT(この場合はYかZ)だけを取り出したリストを返すメソッドがあると。

さらに別のクラスがあって,こんなことがしたい。

abstract class AbstractClass<T extends X> {
  public void hoge(XList xlist) {
    List<T> list = xlist.getElementsByClass(T); // コンパイルエラー
    // list を使ってなんか処理する
  }
}

// X, Y, Z について特殊化したクラスをいくつか定義する ← これがやりたい
class DoWithX extends AbstractClass<X> {}
class DoWithY extends AbstractClass<Y> {}
class DoWithZ extends AbstractClass<Z> {}

Javaの型パラメータはコンパイル時に用いられるだけで実行時には消えてしまうらしいので,この"xlist.getElementsByClass(T);"がエラーになるのはどうやら仕方がないことらしい。

AbstractClassの型パラメータをなくして,コンストラクタにClass型のオブジェクトを渡すってやり方にすれば解決できるんだけど,なぜかその場合でも「AbstractClassに型パラメータを持たせた方が良い」みたいな警告がでてしまった。結果,継承する側は型名を2回書かなければならず,なんかエレガントじゃないのでこの方法は却下。

最終的に,[Java][Generics] 型パラメータで指定されたクラスを取得する。 - うなの日記を参考にして以下のように書き直した。

abstract class AbstractClass<T extends X> {
  private Class<? extends X> classObj; // ほんとは Class<T> classObj にしたい
  AbstractClass() {
    updateClassObj();
  }

  @SuppressWarnings("unchecked") // 警告を抑制するアノテーション
  private void updateClassObj() {
    Type type = this.getClass().getGenericSuperclass();
    if (type != null && type instanceof ParameterizedType) {
      ParameterizedType pType = (ParameterizedType) type;
      classObj = (Class<T>) pType.getActualTypeArguments()[0]; // アノテーションが無い場合,コンパイラに「Unchecked cast」と警告される
    } else {
      classObj = X.class;
    }
  }

  public void hoge(XList xlist) {
    List<T> list = xlist.getElementsByClass(classObj);
    // list を使ってなんか処理する
  }
}

// 意図通りに動作する
class DoWithX extends AbstractClass<X> {}
class DoWithY extends AbstractClass<Y> {}
class DoWithZ extends AbstractClass<Z> {}

リフレクション用の関数を使えば型パラメータを実行時に取得できるよ!ってことらしい。

一応やりたいことは実現できたのだけどなんだかめんどくさいし,あんまり安全じゃなさそう。
それから釈然としないのは,

Class<T> classObj;

とすると

classObj = X.class;

のところで「Type mismatch: cannot convert Class to Class」というコンパイルエラーが出るところ。
ここでは「T = ? extends X」だから問題無さそうなのに。謎。謎。