再帰的ジェネリクス
クラスHogeがあったとして、型変数Tを取る。
public class Hoge<T> {}
public class Hoge<T extends Hoge> {}
すると、T extends Hoge の Hoge が raw型だと警告される。Hogeの<>の部分にHoge型を継承した型を指定しなければならない。ここで型変数T が extends Hogeだったので、丁度いいからT型をおさめよう。
public class Hoge<T extends Hoge<T>> {}
これは再帰的ジェネリクス(recursive generics)と呼ばれているようだ。
追記:僕は勝手に自己言及型ジェネリクスなどと呼んでいた。情報サンクス!併せてタイトルなども表現を「再帰的ジェネリクス」に統一しました。
再帰的ジェネリクスの利用方法
さて、このようにして宣言されたHoge型だが、どうやって使うのだろうか。ちょっとHoge型の変数hogeを宣言してみようじゃないか。
Hoge hoge = null;
当然ながらHogeの型変数を指定しないraw型なのでコンパイル警告がでる。型変数Tを指定すればいいんだろう。TはHogeをextendsしてるわけだからHogeを入れてみる。
Hoge<Hoge> hoge = null;
すると、以下のようなエラーが出る(Eclipse3.6使用)
Bound mismatch: The type Hoge is not a valid substitute for the bounded parameter <T extends Hoge<T>> of the type Hoge<T>
しょうがないので
Hoge<?> hoge = null;
使えないじゃん。このクラスHogeのnewも考えてみよう。
new Hoge();
これはraw型なので警告される。
new Hoge<?>();
…と思うのは早計だ。こいつはextends して使って初めて真価を発揮する。
public class Piyo extends Hoge<Piyo> {}
ここでHoge型にメソッドを足してみよう。
public class Hoge<T extends Hoge<T>> { public void hoge(T hoge) {} }
そしてPiyoでこのメソッドをオーバーライドしてみよう。EclipseならPiyo型でhogeとタイプしてCtrl+spaceだ。
public class Piyo extends Hoge<Piyo> { @Override public void hoge(Piyo hoge) { // TODO Auto-generated method stub super.hoge(hoge); } }
注目するのはhogeメソッドの引数の型だ。具象型のPiyo型になっている。スーパークラスのHoge型は当然ながらPiyoクラスなんてものは知らないわけだが、そのPiyoクラスでオーバーライドされたメソッドの引数はPiyo型という具象型にすることができるわけだ。
これを利用して
public class Hoge<T extends Hoge<T>> { public void hoge(T hoge) {} public void loop(List<T> list) { for (T hoge : list) { hoge.hoge(hoge); } } }
のようにループを書くこともできる。単純なループに限らず、複雑な処理を書く事もできる。つまりTemplate Method パターンを抽象化することができる。*3
さらに継承する
ところで、このPiyo型を継承した場合にちょっと困る。
public class Fuga extends Piyo { @Override public void hoge(Piyo hoge) {} }
Piyo型がHoge<Piyo>型を継承しているわけだから、Hoge型の型変数TはPiyo型で固定になっている。だから、Piyoを継承したFugaでもHoge型の型変数TがあったところはPiyo型なのだ。Fuga型にはならない。
これを回避するために新たな型変数を導入したPiyo2型を作ってみよう。
public class Piyo2<P extends Piyo2<P>> extends Hoge<P> { public void hoge(P hoge) { hoge.piyo(); } public void piyo2() {} }
Hoge型のTはextends Hogeだったが、ここではextends Piyo2である型変数P を作った。PはPiyo2を継承しているわけだから、P型の変数に対してpiyo2()を呼出すことができている。このPiyo2型を継承してFuga2型を作ってみよう
public class Fuga2 extends Piyo2<Fuga2> { @Override public void hoge(Fuga2 hoge) {} }
となって、hoge()の引数が具象型Fuga2になった。
代入の問題
さて、これで
Hoge
├ Piyo - Fuga
└ Piyo2 - Fuga2
という継承関係ができた。
Hogeの型変数Tは
- Piyo : Piyo
- Fuga : Piyo
- Piyo2 : P(extends Piyo2)
- Fuga2 : Fuga2
となっている。そんなHogeファミリーを型変数として使いたいクラスFooが登場したとしよう。
public class Foo<X extends Hoge<X>> {}
このFoo型の変数を宣言してみようじゃないか。
Foo<Piyo> piyo; // OK Foo<Fuga> fuga; // NG Foo<Piyo2> piyo2; // NG Foo<Fuga2> fuga2; // OK
ここでFuga、Piyo2はコンパイルエラーとなる。なぜだろうか。
Fooの型変数Xは、extends Hoge
- Piyo : Piyo
- Fuga : Piyo
- Piyo2 : P(extends Piyo2)
- Fuga2 : Fuga2
つまり、Foo<X extends Hoge<X>>という境界の場合、Xの部分は同じでなくてはならず、Fuga extends Hoge<Piyo>のようなクラスは型が一致しないのだ。
境界をゆるめる
Piyo型を継承したFuga型が使えないってのは都合が悪い。そこでFooより境界をゆるめたFoo2を作ってみよう。
public class Foo2<X extends Hoge<?>> {}
Foo<Piyo> piyo; // OK Foo<Fuga> fuga; // OK Foo<Piyo2> piyo2; // OK Foo<Fuga2> fuga2; // OK
こんどはどれもコンパイルすることができた。
ところで、Hogeを継承したこんなクラスを作ることができる。
public class EvilPiyo extends Hoge<Piyo>{ @Override public void hoge(Piyo hoge) {} }
EvilPiyoではHoge型の型変数Tに対してEvilPiyoを指定するのではなく、Hogeを継承した別の型Piyoを指定している。
このことが示しているのは、Hoge<T extends Hoge<T>>という型変数の宣言は、スーパークラス側から指示してサブクラスにサブクラス自身の型を扱わせることができるが、完璧に統制することが出来るわけではないということだ。
このEvilPiyoはまさにその穴を突いた存在なのだ。規則を緩めたFoo2はこのEvilPiyoを型システムで排除できない。
Foo<EvilPiyo> epiyo; // コンパイルOK
境界を厳密にする
EvilPiyoは排除したいが、Fugaは許容したい。Foo<X extends Hoge<X>>ではHogeの型変数Tに渡す型とXの型が一致してなくてはならなかった。Fugaはextends Piyoだが親子関係であって同一の型ではない。そこで、
public class Foo3<X extends Hoge<? super X>> {}
としてみよう。XがFugaの場合、Hogeの型変数Tが? super X、つまり? super Fugaであればいいわけだから、Hoge
Foo<Piyo> piyo; // OK Foo<Fuga> fuga; // OK Foo<Piyo2> piyo2; // NG Foo<Fuga2> fuga2; // OK Foo<EvilPiyo> epiyo; // NG
残念なのはPiyo2がNGになってしまうこと。そもそもこれはFoo<Piyo2>のPiyo2がraw型なのがまずい。Piyo2<P extends Piyo2<P>> extends Hoge<P>であるから、Foo<Piyo2<Piyo2<Piyo2<...>>>>というのを表現できない。
型変数の不満
そんなわけで、変数宣言でのFoo<Piyo2<Piyo2<Piyo2<...>>>>の極限みたいなのを今のJavaのジェネリクスでは表現できない。一旦継承した型を作る必要がある。
そして、Hoge<T extends Hoge<T>>のような技法は、サブクラス自身の型を扱いたいという目的を叶えるためのテクニックではあるが、EvilPiyoの例のような型システム的な抜けがあることを指摘した。
つまるところ、Hoge<T extends Hoge<T>>のような技法が、簡易なキーワードで容易に宣言できればよいのだと思う。それは単なるHoge<T extends Hoge<T>>のシンタックスシュガーではなく、EvilPiyoのようなものを排除した安心出来る型変数である必要がある。