11/10に開催されたJJUG CCC 2012 Fallでジェネリクスについてセッションを行いました。
このエントリはセッション内容を補足するものです。基礎的な内容は
Javaジェネリクス再入門 - プログラマーの脳みそ
ジェネリックな設計 基礎編 - プログラマーの脳みそ
などを参照してください。
再帰的ジェネリクス
public class Hoge<T extends Hoge<T>>
このように、型変数Tに対して境界を宣言したクラスHoge自身にした型変数をもつかたちを指して再帰ジェネリクス・再帰的ジェネリクスなどと呼びます。
このような型変数Tはパラメータ化された型として変数宣言に用いようとすると
Hoge<?> hoge;
といったようにワイルドカードを用いないと宣言できません。また、newによって型をバインドしようとしても型をバインドすることができません。
再帰的な型変数をバインドするには継承による型のバインドをする必要があります。
public class Piyo extends Hoge<Piyo> {}
導入の動機
次のように抽象クラスHogeを定義します。
public abstract class Hoge { public abstract Hoge getSelf();
Hoge型を継承した具象クラスでは、このgetSelf()メソッドを実装する必要があります。
public class Piyo extends Hoge { @Override public Hoge getSelf() { return this; } }
このPiyo型を利用するコードは
Piyo piyo = new Piyo();
Piyo piyo2 = (Piyo)piyo.getSelf();
このように自身の型を返すというメソッドであるにも関わらず、キャストを必要としてしまいます。
ここでHoge型に型変数を用意してみましょう。
public abstract class Hoge<T> { public abstract T getSelf(); } public class Piyo extends Hoge<Piyo> { @Override public Piyo getSelf() { return this; } }
型変数を導入することでPiyo型のgetSelf()の戻り値の型をPiyo型にすることができました。しかし、Hoge型の型変数Tは無制限なので何型でもバインドできてしまいます。なので、TにHoge自身を継承するという制約をつけましょう。
public abstract class Hoge<T extends Hoge>
するとHogeがraw型であるという警告が出ると思います。T extends HogeのHogeは型変数Tを持たないといけないのに記述がないからです。ここにいま宣言したTを再帰的に使用してパラメータ化された型としましょう。
public abstract class Hoge<T extends Hoge<T>>
すると、Piyo型で継承時にバインドしたい場合はPiyo型自身をバインドしないと型が合わないことになります。
これによって「具象型のサブクラス自身の型を意味する型変数T」を得ることができました。
使用例
この再帰ジェネリクスを使用している例としてjava.lang.Enumクラスを挙げましょう。以下は抜粋コードです
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable { public final int compareTo(E o) {...} }
Enumクラスはすべてのenumのスーパークラスとなる型です。このようにcompareTo()が定義されているのでenum使用時は型安全にcompareTo()することができます。具体的には引数に別のenumの定数を渡そうとするとコンパイルエラーになります。
このように、再帰ジェネリクスを利用すると、親クラスでの実装しかないメソッドであるにも関わらず、サブクラス側ではサブクラス自身の具象型をIn/Outできるメソッドを定義することができるのです。