高階関数というものをご存知か。関数自身を引数あるいは戻り値に取る関数のことである。「高階」は「こうかい」と読む。その昔「たかしな」と読んだ人がいたとか、いないとか。
先のエントリ「HttpSessionを型安全にする」では「javax.servlet.http.HttpSessionのsetAttribute()/getAttribute()のようなモノをどうやって型安全にするか」という動機付けから、
public class KeyValue<K> { public <T> void put(K<T> key, T value){} }
といったことをやろうとして、Javaの言語仕様上、型変数に型変数を持たせることができないので実現できないと述べた。
簡単に解説すると、ここでKayValueはHashMap的なキーを渡せば値を返すオブジェクトで、型変数Kはキーの集合体を表現している。typesafe enumパターンでキーをあらかじめ列挙しておくわけだ。その列挙されるキーによって返す値の型、つまりT型が切り替わるというのがやりたい。
その型変数に型変数を持たせる――いわば、高階型変数のようなことができれば、このようなシチュエーションを型安全に記述できるかもしれないね、ということを投げっぱなしにして終わっていた。
高階インスタンス
ところで、Javaには高階インスタンスとでもいうべき機能が備わっているのをご存知か。通常のインスタンスはクラスをもとにインスタンスが生成され1対多となる。クラスではなくインスタンスをもとにインスタンスを作る――つまり、エンクロージング型の内部クラスのことである。
public class Outer { public class Inner{} }
このような構造があったとして
Outer o1 = new Outer(); Inner i11 = o1.new Inner(); Inner i12 = o1.new Inner(); Outer o2 = new Outer(); Inner i21 = o2.new Inner(); Inner i22 = o2.new Inner();
といった生成を行える。インスタンスがクラスのもつstaticなフィールドにアクセスできるように、内部クラスは外部のクラスのインスタンスフィールドにアクセスできる。
そして、内部クラスは外部のクラスの型変数をも用いることができる。これを活用できないだろうか。高階には高階を*1。類似の構造を持つならばこそ解決の糸口たりうる。
step1 基本構造
スタート地点を確認しよう。
public class KeyValue { public <T> void put(Key<T> key, T value){ /** 実装省略 */ } } public final class Key<T> { public static final Key<String> KEY_1 = new Key<String>(); }
TypesafeEnumパターンで列挙型Keyを表現している。型変数をもち、定数KEY_1を作る段でデータの型をStringと定めている。
KeyValueクラスではメソッドスコープの型変数 T を利用して、Keyが定めるデータの型と、実際のvalueの型が等しいことを表している。
ここではKey型を具象型で表現したが、この部分をKeyValue型の型変数にしてしまいたい。そうすることでKeyValue型は具象型Keyに列挙されたキーだけを扱う専用クラスではなく、任意の列挙をキーとする汎用型にすることができる。
step2 型の多階層化
Javaの型変数は型変数を持てないが、インスタンスのインスタンスである内部クラスは型変数を持つことが出来る。Key型を2階層の型に差し替えよう。
public class KeyValue { public <T> void put(KeyGroup.Key<T> key, T value){} } public class KeyGroup { private static final KeyGroup singleton = new KeyGroup(); public class Key<T> {} public static final Key<String> KEY_1 = singleton.new Key<String>(); }
元のKey型をKeyGroupとKey型の2階層に置き換えた。
定数の宣言には親となる外部クラスのインスタンスが必要となる。ここではKeyGroupをシングルトンパターンとし*2、そのインスタンスで定数を宣言した。
step3 外部クラスを型変数に置き換える
では外部クラスを型変数に置き換えてみよう。
public class KeyValue<KG extends KeyGroup> { public <T> void put(KG.Key<T> key, T value){} }
驚くことにこれ、コンパイルできてしまった(Eclipse3.6で確認。JDKだと通らないかもしれない。未確認)。ポイントはKG.Keyで、型変数.内部クラスという表現が通るなんて。しかし、やはりどうも胡散臭い。というのも
KeyValue<KeyGroup> tm = new KeyValue<KeyGroup>(); tm.put(KeyGroup.KEY_STRING, "value");
はコンパイルできるのだが、KeyGroupを継承した型Hogeを作って
public class Hoge extends KeyGroup { private static final Hoge singleton = new Hoge(); public static final Hoge.Key<String> HOGE_STRING = singleton.new Key<String>(); }
このHogeを使って
KeyValue<Hoge> tm2 = new KeyValue<Hoge>(); tm2.put(Hoge.HOGE_STRING, "hoge");
とやろうとするとputメソッドで型エラーとなるのだ。
The method put(Hoge.Key<T>, T) in the type KeyValue<Hoge> is not applicable for the arguments (KeyGroup.Key<String>, String)
というエラーメッセージからすると、どうもKG.KeyのKGはKeyGroup型扱いになってしまっているようだ。型変数を外部クラスとして内部クラスを表現できるなんておかしいと思ったんだ。
step4 外部クラスに再帰的型変数を導入する
直接的に外部クラスを型変数に置き換えるのでは駄目だったので、外部クラスに再帰的型変数を持たせることで型の解決を図る。
再帰的型変数については再帰的ジェネリクスの代入互換性を参考にするといい。
public class KeyGroup<S extends KeyGroup<S>> { public class Key<T> {} }
このように再帰的な型変数Sを導入しておいて、KeyGroupを継承したHogeは
public class Hoge extends KeyGroup<Hoge> { private static final Hoge singleton = new Hoge(); public static final Hoge.Key<String> HOGE_STRING = singleton.new Key<String>(); }
という感じになる。さてKeyValueクラスはどうなるか。
public class KeyValue<KG extends KeyGroup<KG>> { public <T> void put(KeyGroup<KG>.Key<T> key, T value){} }
前回のKG.Key<T>がKeyGroup<KG>.Key<T>になったわけだ。使う際は
KeyValue<Hoge> tm = new KeyValue<Hoge>(); tm.put(Hoge.HOGE_STRING, "hoge");
ってな感じ。不可能と思っていた昨日までの自分よ、さようなら!
step5 蛇足
さて、この例は再帰的ジェネリクスの代入互換性でとりあげたケースに該当するので、Hogeを継承したExHogeクラスを作るとKeyValueの型変数の境界<KG extends KeyGroup<KG>>にマッチしない。
そこで例の? superを用いる境界に直すと
public class KeyValue<KG extends KeyGroup<? super KG>> { public <T> void put(KeyGroup<KG>.Key<T> key, T value){} }
となるのだが、ここでコンパイルエラーとなってしまう。
Bound mismatch: The type KG is not a valid substitute for the bounded parameter <S extends KeyGroup<S>> of the type KeyGroup<S>
これはニュアンスとしてはKeyGroup<Hoge>であるところがKeyGroup<KeyGroup<Hoge>>となってしまって型が合わないということ。再帰的型変数を使っていると時折遭遇する厄介な問題である。