Javaのジェネリクスの話題のひとつにnew T()したいができないので困る、というテーマがある。
先日のセッションではこの対策についても簡単に述べたが今日はそのまとめ。
まず第一に疑うべきは本当にnew T()する必然性があるか?というのが持論だが、ある種のフレームワークではその必然性がある。
(このあたりの議論はJavaのジェネリクスで,T.class や new T() ができず悩んだ話 (型パラメータのインスタンス化に関し、フレームワーク設計からケーススタディ) - 主に言語とシステム開発に関してによくまとまっている)
ここで、これらのPOJO(Plain Old Java Object - 端的に言えばデータを格納するだけの単純なオブジェクト)は、デフォルトコンストラクタ(引数を持たないコンストラクタのこと)を持つという前提を受け入れることとしよう。
このとき、どのようなメソッドの形状となるのだろうか。
引数にClassをもらう方式
一般的な妥協案として採用されているのは引数にjava.lang.Classを受け取る手法である。
public static <T> T findById(String id, Class<T> clazz) { try { return clazz.newInstance(); } catch (ReflectiveOperationException e) { throw new RuntimeException(e); } }
このとき、呼び出し元のコードはジェネリックメソッドへのバインドを明示的に書くとすれば
Hoge hoge = Sample1.<Hoge>findById("id-hoge", Hoge.class);
といった形になる。ここで、ジェネリックメソッドへのバインドは型推論が効くので普段は型推論を頼りに以下のように書くだろう。
Hoge hoge = Sample1.findById("id-hoge", Hoge.class);
staticメソッドで定義されたAPIであれば、変数宣言と引数に渡すClassとの2カ所で型を記述するのは、よく訓練されたJavaプログラマであれば許容範囲と考えるかもしれない。*1
Daoクラスを定義する場合
先の例ではstaticメソッドで提供する想定としていたが、対象エンティティごとにDaoクラスを作成するケースを想定してみよう。
この場合BaseDaoで共通の実装をして、具象型別の処理はそれぞれの具象Daoで実装することになろう。
/** Daoの共通実装部 */ public class BaseDao<T> { public T findById(String id, Class<T> clazz) { try { return clazz.newInstance(); } catch (ReflectiveOperationException e) { throw new RuntimeException(e); } } } /** Hogeの具象Dao */ public class HogeDao extends BaseDao<Hoge> {}
このとき、利用側は
HogeDao hogeDao = new HogeDao(); Hoge hoge = hogeDao.findById("id-hoge", Hoge.class);
となる。わざわざHogeDaoを作っているのに引数にまたHoge.classを渡すことになっている。
一応、BaseDaoとHogeDaoに以下のような記述を足せばこれは削ることができる
public abstract class BaseDao<T> { /** 実装はprotectedに */ protected T findById(String id, Class<T> clazz) { try { return clazz.newInstance(); } catch (ReflectiveOperationException e) { throw new RuntimeException(e); } } /** 引数にClassをとらない実装は具象型で */ public abstract T findById(String id); } public class HogeDao extends BaseDao<Hoge> { @Override public Hoge findById(String id) { // 具象型でHoge.classを渡す return findById(id, Hoge.class); } }
しかし、これでは具象DaoでClassを渡すためだけの実装をそれぞれ行わなければならない。
NGパターン
さて、ここでNGパターンをいくつか見ておこう。static版で引数でClassを受け取るのを削ろうとしたパターン。
public static <T> T findById(String id) { // NG1 : 型変数TからClassはとれない Class<T> clazz = T.class; // NG2 : new T()できない T t = new T(); // ... }
このあたりは文法的にそもそもそんな機能はない。ない袖はふれないケース。
便利にならないオブジェクト渡し
引数にClassではなくオブジェクトをもらう方式
public static <T> T findById(String id, T dummy) { Class<T> clazz = dummy.getClass(); // ... }
結局、オブジェクトからClassをとってきてリフレクションでインスタンスを生成するわけだが、dummyはnull値ではだめなので、呼び出し側はいちいちnewして渡すなりしなくてはならない。だったらClass渡すほうが楽なのだけど、クラス名.classという記法を知らない人がうっかりこういう実装を書くことがある。
可変長引数によるオブジェクト渡し
一応実現可能だが制約がいろいろある裏ワザ。
public static <T> T findById(String id, T ... ta) { try { Class<?> clazz = ta.getClass(); Class<?> componentType = clazz.getComponentType(); return (T) componentType.newInstance(); } catch (ReflectiveOperationException e) { throw new RuntimeException(e); } }
引数の宣言にTの可変長引数を宣言しておく。これを呼び出す側では
Hoge hoge = Sample1.findById("id-hoge");
というように引数taの部分を記述しない。これにより可変長引数に空の配列が渡される。そこからgetComponentType()で配列の要素の型を取得し、newInstance()に漕ぎ着ける…という手法。
この時、呼び出し側では見えない可変長引数に具象型を渡さなくてはならない。一段抽象化して型変数を用いるなどすると破たんする。コンパイル時に静的に呼び出す型がわかっていなければ可変長引数への空配列渡しで具象型の空配列が渡らないのである…。
また、EclipseなどのIDEはこの可変長引数部分の変数を補完してくれちゃって、逆に削らないといけなかったりと結局はユーザは便利に使えないということになる。
解決策
さて、だいぶん勿体つけたが、解決編に入ろう。型変数Tからは実行時にバインドされる型を得ることはできない。実行時の型を取りたければダミーのオブジェクトを受け取るか。しかしダミーを受け取るぐらいならClassをもらうほうがマシだ…。
いやいや、ひとつ忘れてますよ。T型が何かを知っているオブジェクトをひとつ。
public abstract class BaseDao<T> { public T findById(String id) { try { // 実行時の型が取れる。ここではHogeDaoなど Class<?> clazz = this.getClass(); // ここではBaseDao<Hoge>がとれる Type type = clazz.getGenericSuperclass(); ParameterizedType pt = (ParameterizedType)type; // BaseDaoの型変数に対するバインドされた型がとれる Type[] actualTypeArguments = pt.getActualTypeArguments(); Class<?> entityClass = (Class<?>)actualTypeArguments[0]; // リフレクションでインスタンスを生成 return (T) entityClass.newInstance(); } catch (ReflectiveOperationException e) { throw new RuntimeException(e); } } }
Tが何型かを知っているインスタンスとはthisのことである。
HogeDaoのfindById()が呼ばれたのであればこのthisとはHogeDaoのことであり、HodeDaoはBaseDaoのTに何がバインドされたかを知っている。なのでthisを問いただせばTにバインドされた具象型を知ることができ、メソッドの引数でいちいちClassを渡す必要はないのである。
ここのサンプルでは簡易に取得しているが、多段継承している可能性もあり、実際に型変数に何がバインドされているかを適切なエラー処理をしつつ記述することは難しい。
このあたりの詳細は
を参考にしてほしい。