引数と戻り値の不一致 - ジェネリクス・ケーススタディ

ある型のインスタンスを受け取り、Listにして返すメソッドを考えよう。

	public static <T> List<T> wrap(T value) {
		List<T> list = new ArrayList<T>();
		list.add(value);
		return list;
	}

このとき、型変数はメソッドのIn / Outで型の関連を表現できさえすればよいので、型変数のスコープはメソッドスコープでよい。

例示のために継承階層をもったクラスA,B,Cを用意しておく。

public class A {}
public class B extends A {}
public class C extends B {}

ではこのwrapメソッドの呼び出し側のコードがどうなるかを見てみよう。

public class Sample {
	public static void main(String[] args) {
		List<A> wrap1 = wrap(new A());
		List<B> wrap2 = wrap(new B());
		List<A> wrap3 = wrap(new B()); // Java7 では error
		List<A> wrap4 = wrap((A)new B());
		List<A> wrap5 = Sample.<A>wrap(new B());
	}
}

wrap1, wrap2 のケースでは、渡した引数の型とList型のパラメータが一致するので問題はおきない。

wrap3,wrap4,wrap5のように渡すインスタンスはB型だが、List<A>型で受け取りたいというケースでは不便が生じる。

代入互換性

wrap3はJava7ではコンパイルエラーとなる。

引数がB型であるから、wrapメソッドの型変数TはBであると推論され、戻り値はList<B>型とされる。ここで、パラメタライズドタイプのパラメタは非変(nonvariant)であることを確認しておこう。

Javaジェネリクス再入門 - プログラマーの脳みそから部分的に抜粋する。

Hoge<A> a = new Hoge<B>(); // コンパイルエラー!

なぜダメなのか。ArrayListで考えてみよう。

ArrayList<B> bList = new ArrayList<B>();
ArrayList<A> aList = bList; // 本来は代入できないができたと仮定する
aList.add(new A()); // ArrayList<A>にはA型を代入できる
B b = bList.get(0); //ArrayList<B>なのでget()の結果はB型のはず

ここで、aList = bListなので、aListにadd()したA型のインスタンスが、bListからget()できてしまう。ArrayList<B>なのでget()の結果はB型のはずだが、B型より上位のA型がとれてしまった。これではClassCastExceptionになってしまう。

Java8の型推論強化

Java8ではジェネリクス型推論が強化された。ラムダ式の導入にあたってJava7までの型推論器の挙動では非常に不便であるとされたのである。

List<A> wrap3 = wrap(new B());

この場合、左辺であるList<A>を元に推論したならば、wrapメソッドの型変数TにAをバインドすると推論してコンパイルすることができる。Java8ではこのような左辺(というかメソッドのreturnの型)からの推論がされるようになった。

変数への代入の他、メソッドの引数に別のメソッドの戻り値を渡すようなケースでも、外側のメソッドの引数の型から推論される。

Java7での対応

話を戻そう。Java7は2015年4月をもってEOL(サポート終了)だが、未だにJava7(あるいはもっと古いバージョン)で開発をしているケースもあるだろう。

そうしたケースでwrap3のような場合にどう対応するのか。

List<A> wrap3 = wrap(new B()); // Java7 では error
List<A> wrap4 = wrap((A)new B());
List<A> wrap5 = Sample.<A>wrap(new B());

wrap4では引数からの型推論にヒントを与えるためにB型をA型にキャストして型をコンパイラに教えている。呼び出し側から行う対応として一番楽なのはこの方法だろう。

wrap5では型変数に対して推論ではなく明示的にバインドを行っている。
メソッドスコープの型変数へのバインドはピリオドの後ろ、メソッド名の前に<>で記述する。この時、staticメソッドであればピリオドを記述するためにクラス内からのアクセスであっても型名を省略できない。
インスタンスメソッドの場合でも同インスタンス内からであればthis.を省略できない。

メソッドの宣言側の工夫

ここで、メソッド宣言側でやれる工夫がある。

	public static <R, T extends R> List<R> wrap(T value) {
		List<R> list = new ArrayList<R>();
		list.add(value);
		return list;
	}

引数の型を表す型変数Tをそのまま戻り値の型Listに使っていたので自由が効かなかった。そこで、引数の型を表す型変数Tと戻り値の型を表す型変数Rに分離し、TとRの関係をT extends Rと規定した。

これによりJava7でも

List<A> wrap6 = wrap(new B());

という記述が推論されるようになる。