前回から随分と間が開いてしまった。Javaのジェネリクスはイレイジャ方式だけどもクラスにバインドした型の情報が残る場合があるんだよシリーズの第2弾である。
具体的にどういう場合にバインドした型が残るのかという話だが、端的に列挙すると以下のものである。
- フィールドにパラメタライズドタイプ(parameterized-type)を用いた場合
- メソッド引数や戻り値型にパラメタライズドタイプを用いた場合
- コンストラクタにパラメタライズドタイプを用いた場合
- 継承によるバインドを行った場合
大きく分ければパラメタライズドタイプを使ったメンバのシグネチャか、継承によるバインドの2種類である。
前回、例に挙げたのはパラメタライズドタイプを引数にとるケースだった。
import java.lang.reflect.*; import java.util.List; public class ReflectionTest { public static void main(String[] args) throws Exception { Method m = ReflectionTest.class.getMethod("hoge", List.class); Type[] types = m.getGenericParameterTypes(); for (Type type : types) { System.out.println(type); } } public void hoge(List<String> list){} }
ここでおさらいしておくと、パラメタライズドタイプというのは「パラメータ化された型」という面白みのない訳語があてられるのだが、要するに型の宣言である。Javaのジェネリクスは山括弧が出てくるが、型変数の宣言、型変数へのバインド、パラメタライズドタイプの3種類の山括弧があった。
このあたりはJJUGでセッションをしたときの資料を参照されたい。
- セッション資料 : ジェネリクスの基礎と応用 JJUG CCC 2012 Fall
- 資料の補足記事 : ジェネリックな設計 基礎編 - プログラマーの脳みそ
さて、この3種類の山括弧のうち、パラメタライズドタイプについてはclassファイルにその情報が残ることは述べたとおりなのだが、もうひとつ残るパターンがあった。
継承によるバインド
ジェネリクスの型変数に対する型のバインドは2種類あった。
new ArrayList<String>();
と、このようにnewの際に型を示すのがひとつ。もうひとつは
public class Hoge extends ArrayList<String> {}
といったように継承の際にバインドするというものだった。
継承によるバインドはClass#getGenericSuperclass()でjava.lang.reflect.ParameterizedTypeを得ることが出来る。
これにより、型変数に何がバインドされたのかを得ることが出来るのだが、多段に継承がされている場合にsuperの型変数にこのクラスで宣言された型変数をバインドするということが出来てしまうため、きっちり型のバインド関係を得ようとすると再帰的に処理しなくてはならない。
public class A<X> {} public class B<Y> extends A<Y> {} public class C extends B<String> {}
といった階層があった場合に、大本のクラスAの型変数Xになにがバインドされたのかを得たければ
- Aの型パラメータのプレースホルダ
を取得 - Bでsuper-classの型パラメータに渡されたTypeを取得
- 2のTypeが具象型ではなく型パラメータ
なのでさらにサブクラスを走査 - Cでsuper-classの型パラメータに渡されたTypeを取得
- B
のYにString型が渡されていることが分かるのでA のXがStringであることが分かる
というステップを踏むことになる。
以下、サンプルプログラム。
import java.lang.reflect.*; import java.util.Stack; /** * 渡された型から継承階層を登って、 指定の親の型の指定の名前のジェネリクス型パラメータが 継承の過程で何型で具現化されているかを走査して返す。 * * @param clazz * 走査開始する型 * @param targetClass * 走査する対象のジェネリクス型パラメータを持つ型。 走査開始型の親である必要がある。 * @param targetTypeName * 何型で具現化されたを確認したい型パラメータのプレースホルダ名 * @return 具現化された型 */ public static <T> Class<T> getGenericType(Class<?> clazz, Class<?> targetClass, String targetTypeName) { if (!targetClass.isAssignableFrom(clazz)) { throw new IllegalArgumentException("型" + clazz.getName() + "は、型" + targetClass.getName() + "を継承していません"); } Stack<Class<?>> stack = new Stack<Class<?>>(); while (!targetClass.equals(clazz.getSuperclass())) { stack.push(clazz); clazz = clazz.getSuperclass(); } return getGenericTypeImpl(clazz, targetTypeName, stack); } /** * 型パラメータの具象型取得の実装。再帰処理される。 * * @param clazz * 現在の走査対象型 * @param targetTypeName * 現在の走査対象のジェネリクス型パラメータ名 * @param stack * 現在の走査対象型以下の継承階層が積まれたStack * @return 該当型パラメータの具現化された型 */ @SuppressWarnings("unchecked") private static <T> Class<T> getGenericTypeImpl(Class<?> clazz, String targetTypeName, Stack<Class<?>> stack) { TypeVariable<? extends Class<?>>[] superGenTypeAray = clazz .getSuperclass().getTypeParameters(); // 走査対象の型パラメータの名称(Tなど)から宣言のインデックスを取得 int index = 0; boolean existFlag = false; for (TypeVariable<? extends Class<?>> type : superGenTypeAray) { if (targetTypeName.equals(type.getName())) { existFlag = true; break; } index++; } if (!existFlag) { throw new IllegalArgumentException(targetTypeName + "に合致するジェネリクス型パラメータがみつかりません"); } // 走査対象の型パラメータが何型とされているのかを取得 ParameterizedType type = (ParameterizedType) clazz .getGenericSuperclass(); Type y = type.getActualTypeArguments()[index]; // 具象型で継承されている場合 if (y instanceof Class) { return (Class<T>) y; } // ジェネリックパラメータの場合 if (y instanceof TypeVariable) { TypeVariable<Class<?>> tv = (TypeVariable<Class<?>>) y; // 再帰して同名の型パラメータを継承階層を下りながら解決を試みる Class<?> sub = stack.pop(); return getGenericTypeImpl(sub, tv.getName(), stack); } // ジェネリック型パラメータを持つ型の場合 if (y instanceof ParameterizedType) { ParameterizedType pt = (ParameterizedType) y; return (Class<T>) pt.getRawType(); } throw new IllegalArgumentException("予期せぬ型 : " + y.toString() + " (" + y.getClass() + ")"); }
さて、これを応用すると、再帰ジェネリクスで具象型をバインドさせた場合に、抽象クラスから具象型を得ることが出来るようになる。
public class Abstract<T extends Abstract<T>> { public Abstract() { Class<T> clazz = getGenericType( getClass(), Abstract.class, "T"); } }
これだけであれば、getClass()するだけでいいじゃないのという話だが、再帰用のTの他にパラメータを持つようになると応用が効いてくる。
public class Abstract<P, R, T extends Abstract<P, R, T>> { public Abstract() { Class<P> clazzP = getGenericType( getClass(), Abstract.class, "P"); Class<R> clazzR = getGenericType( getClass(), Abstract.class, "R"); } }
ここではパラメータ用の型Pと戻り値用の型Rをとる例を想定してみた。リフレクションでインスタンスを作って値をバインドするようなタイプのフレームワークなどで利用することが出来る。