項目28 APIの柔軟性向上のために境界ワイルドカードを使用する

今日もEffective Java第二版より。なかなか難しかったが、理解できてスッキリ!

Effective Java 第2版 (The Java Series)

Effective Java 第2版 (The Java Series)


境界ワイルドカードには<? extends E>と<? super E>があります。

共変と不変

そもそも境界ワイルドカードが必要なのは、ジェネリクスが「不変」であることによります。ちなみに配列は「共変」。
言葉の定義では分かりにくいので、以下の例で。

Number[] ary = new Long[2];
List<Number> list = new List<Long>(); // コンパイルエラー

つまり、NumberとLongには親子関係があり、配列はその関係が生きているが、List<Number>とList<Long>には親子関係がない(この性質のことを共変・不変と呼んでいる)。よってコンパイルエラーになっている。
ジェネリクス型の不変という性質により、仮にList#addAllの引数がList<E>で宣言されているとすると、

List<Integer> intList = new ArrayList<Number>();
intList.add(10); // 当然OK(new Integer(10)にしなくてよいのはオートボキシングが働いているから)

List<Number> numList = new ArrayList<Number>();
numList.add(5);  // OK

numList.addAll(intList);  // コンパイルエラー

となってしまう。

<? extends E>

そこで、上記の問題を解決するのが境界ワイルドカード型というわけです。
実際、List#addAllの引数はList<? extends E>と宣言されている。これにより、Numberの子クラスであるIntegerのリストが引数として渡せるようになる。
上限境界ワイルドカードともいう。

<? super E>

こっちはちょっと分かりにくいが、どういうところで使用されているかというと、Collections#sort(List<T> list, Comparator<? super T> c)なんかが代表的。

public static void main(String[] args) {
	Comparator<Number> c = new Comparator<Number>(){
		public int compare(Number o1, Number o2) {
			return o1.intValue() - o2.intValue();
		}
	};
	
	List<Double> list = new ArrayList<Double>();
	list.add(5.3);
	list.add(3.2);
	list.add(4.9);
		
	Collections.sort(list, c);
		
	for(Double d : list) {
		System.out.println(d);  // 3.2 4.9 5.3
	}
}

もしCollections#sortの第二引数がComparator<T>だとすると、Listの型パラメータごとにComparatorを用意しないといけなくなってしまう。しかし<? super T>となっていることによって上記の例の様にIntegerやDoubleの親クラスであるNumberクラスに型付けされたComparatorが利用できるようになり、List<Integer>にもList<Double>にも対応できるというわけ。
こちらを下限境界ワイルドカードともいう。

PECSとGet&Put原則

この2つの使い分けの指針として、PECS(Producer - Extends, Consumer - Super)とGet&Putの原則が紹介されています。
個人的にはGet&Putの方が単純で分かりやすかったのですが、Effective JavaではPECSベースで解説がされているため、Get&Putベースで勉強したい人にはこちらの記事がオススメです。
IBM Developers Works : getとputの原則

問題

では、これら2つの性質を理解したところで理解度をチェックする問題を作ってみました。是非トライしてみてください。

public static void add(List<Number> list){
    list.add(5);
}

public static void add(List<? extends Number> list){
    list.add(5);
}

public static void add(List<? super Number> list){
    list.add(5);
}

これら3つのうち、コンパイルエラーになるものを1つ選びなさい。


シンキングタイム・・・
シンキングタイム・・・
シンキングタイム・・・
シンキングタイム・・・
シンキングタイム・・・
シンキングタイム・・・
シンキングタイム・・・
シンキングタイム・・・
シンキングタイム・・・
シンキングタイム・・・

解答

2番目の<? extends Number>がコンパイルエラーになります。

解説

Number型を継承したListなんだからInteger型の5はいけるだろう、と見た目だけで脊髄反射すると間違ってしまいます。
List<? extends Number>という下限境界ワイルドカードで表現される具体的な候補を考えれば間違うことは無いと思います。List<Integer>もその候補ですが、List<Double>というのも候補です。List<Double>にInteger型のインスタンスをaddすることは出来ません。仮にIntegerの子クラスが存在した場合も同様にInteger型のインスタンスをそのIntegerの子クラスに関連付けたListにaddすることも出来ない。よってコンパイラとしてはエラーにするというのが正しい。
一方、List<? super Number>という上限境界ワイルドカードで表現される具体的な候補を同様に考えてみます。こちらはList<Object>が候補となります。よってNumberを継承した子孫クラス達は全部追加できます。5(Integer)でも5.5(Double)でもOKです。