再帰的ジェネリクスの代入互換性

Javaのややこしいジェネリクスの話をしよう。*1

再帰ジェネリクス

クラスHogeがあったとして、型変数Tを取る。

public class Hoge<T> {}

このHogeの型変数Tがextends Hogeとすると

public class Hoge<T extends Hoge> {}

すると、T extends HogeHoge が 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<?>();

これはコンパイルエラー。やっぱり使えないじゃないか*2

…と思うのは早計だ。こいつは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、Fuga、Piyo2、Fuga2はそれぞれextends Hogeではある。だが、extends Hogeだろうか、ということだ。再掲しよう。Hogeの型変数Tはそれぞれのクラスで何になっているか。

  • 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のようなものを排除した安心出来る型変数である必要がある。

*1:本稿は再帰型のジェネリクス型変数の境界の焼直し解説版

*2:ちなみにclass Piyo extends Hoge<Piyo>を作ると、new Hoge<Piyo>();とすることができるようになる

*3:これはフレームワークの設計などでは役立つ技法だが、抽象化したものをさらに抽象化するという話題なので、相当なスケールがないと役立たないことが多い