@masaru_b_cl とジェネリクスの話をしていて、継承の際に型変数に具体的な型をバインドして単純化したりするんだよ、みたいな話をしていたのだけど、コード例がないとイメージしにくいと思うのでちょっと書いておこう。
二つの型変数 I, O をもつinterfaceがあったとする。
public interface Executor<I, O> { O exec(I in); }
この実装クラスとしてList
public class SumInteger implements Executor<List<Integer>, Integer>{ public Integer exec(List<Integer> in) { int ret = 0; for (int value : in) { ret += value; } return ret; } }
このSumIntegerクラスをnewする際にはジェネリクス型変数へのバインドが不要のため、<>がいらない。
SumInteger si = new SumInteger();
この変数の宣言部分を抽象型で宣言したければimplementsの右側と同じ型で受ければいい。
Executor<List<Integer>, Integer> ex = new SumInteger();
かといって、この抽象型を敢えて使う必要性は薄いから、通常は具体的なSumInteger型を用いるだけでいいだろう。型階層の合間で型変数に具体的な値をバインドすることでジェネリクスが複雑になりすぎないようにできる。
部分的な型変数の消去
型変数をすべて消し去るばかりではなく、1個だけ消すようなこともできる。
先にIn と Out の型を型変数としてとるExecutorインターフェースを取り上げた。java.lang.Object#hashCode()のようにハッシュ値を計算するようなHashCodeExecutorインターフェースを考えてみよう。
public interface HashCodeExecutor<I> extends Executor<I, Integer> {}
Executorを継承したinterfaceを作ったわけだが、HashCodeExecutor宣言部分で型変数 I が宣言されている。これを Executorの I にバインドしている。これは型変数なのでHashCodeExecutorを実装するクラスでは型をバインドしなくてはならない。
public class StringHashCodeExecutor implements HashCodeExecutor<String> { public Integer exec(String in) { return in.hashCode(); } }
このように、継承に際して親の型変数に子側の型変数をバインドしてスルーすることもできれば、具体的な型を渡してバインドすることで、子側の型変数を減らすこともできる。
型変数の増加
子側の実装で新たに型変数を追加したいことも時にはあるかもしれない。仮にInにMap
public interface MapExecutor<K, V, O> extends Executor<Map<K, V>, O> {}
継承あるいは実装するにあたって、別種の型変数を用いたくなるというケースはある。型変数は必ず親に渡さなければならないというわけでもないから、別途拡張して追加したメソッドだけで用いたい型変数などを追加することもできる。
public interface ExExecutor<I, O, X1, X2, X3> extends Executor<I, O> {}
かといってどんどん増えると、使う際に何型をバインドしていいものやら分らなくなりがちなので、増えすぎないようにしたい。
型変数の境界の変更
ここまでは型変数には境界を設定していなかった。境界を設定している場合、型変数の代入互換性の仕様に基づき*1、代入可能な範囲で境界を狭めることができる。
C extends B, B extends A という場合、
public class Hoge<T extends A> {} public class Hoge2<T2 extends B> extends Hoge<T2> {} public class Hoge3<T3 extends C> extends Hoge2<T3> {}
ということができる。
継承に際して親側の型変数に具体的な型をバインドするだけでなく、範囲を実装の都合に合わせて適切に狭めるような選択肢もあるということだ。
複雑な境界
また、複雑なジェネリクスがあった場合、実装に際して型階層が間に入ると極シンプルになる場合もある。
/** 値を表現するためのインターフェース */ public interface Value<X, V extends Value<X, V, T>, T extends ValueType<X, V, T>> { T getValueType(); X getValue(); } /** 型を表現するためのインターフェース */ public interface ValueType<X, V extends Value<X, V, T>, T extends ValueType<X, V, T>> { V getMax(); V getMin(); }型に属する情報をジェネリックに扱う試み - プログラマーの脳みそ
みたいな型があって、これを型変数として受け取るクラスHogeを考えよう。(2014/04/22修正。コードに誤りがあった)
public class Hoge< X, V extends Value<X, V, T>, T extends ValueType<X, V, T> >{}
もううんざりだろう。
だが、とくに同記事に出てくる実装例 IntegerValue, IntegerValueType だけを用いたいIntegerHogeの場合、
public class IntegerHoge<V extends IntegerValue, T extends IntegerValueType>{}
だけで済む。
型階層を設け、適時ジェネリクス型変数を消していくようにするとジェネリクスの境界の表現が複雑になりすぎない。型の設計をする場合、こうした利便性も考慮に入れて継承階層を考えたいところ。
Javaを使うならとことん型システムを活用したいよね。さらにはEclipseのCtrl+1の補完のパワーも把握した上で、設計側に苦労をもってきて、利用側が便利になるように配慮したい*2。
*1:これがややこしいのだが…。ジェネリクスの代入を理解する その1、ジェネリクスの代入を理解する その2