31.APIの柔軟性のために境界ワイルドカードを使用すべし
Item28でもあったように、型パラメータはinvariantである。よって、例えば、List<String>
はList<Object>
のサブタイプではない。List<Object>
のできることすべてが、List<String>
にできるわけではないので、リスコフの置換原則にも符合する。(Item10)
時には、もっと柔軟性が必要となることがある。Item29で使用したStackクラスを例に考えてみる。StackクラスのAPIは以下のよう。
public class Stack<E> {
public Stack();
public void push(E e);
public E pop();
public boolean isEmpty();
}
ここに要素のシーケンスを引数にとり、それらをすべてStackにのせるメソッドを追加することを考える。最初に考えるのは以下のようなものだろう。
// pushAll method without wildcard type - deficient!
public void pushAll(Iterable<E> src) {
for (E e : src)
push(e);
}
このメソッドはコンパイルできるが、満足いくものではない。Iterable<E> src
の要素がStackの要素と一致すればうまく動くが、例えば、以下のようなコードはエラーとなる。エラーの理由は、型パラメータはinvariantだからである。
Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = ... ;
numberStack.pushAll(integers);
ここで出てくるエラーに対処するためには、型引数にワイルドカードを用いる。
// Wildcard type for a parameter that serves as an E producer
public void pushAll(Iterable<? extends E> src) {
for (E e : src)
push(e);
}
この対応によって、コンパイルできるようになり、タイプセーフにもなる。
次に、collectionを引数にとり、Stackにたまっている要素をすべてそれに移す、popAll
メソッドを考えてみる。素案は以下のよう。
// popAll method without wildcard type - deficient!
public void popAll(Collection<E> dst) {
while (!isEmpty())
dst.add(pop());
}
これは要素EがStackのそれと完全に一致した場合にはうまく動くが、そうでない場合は動かない。つまり、以下のコードはコンパイルエラーとなる。
Stack<Number> numberStack = new Stack<Number>();
Collection<Object> objects = ... ;
numberStack.popAll(objects);
対処するためには、以下のようにここでも型引数にワイルドカードを用いる。
// Wildcard type for parameter that serves as an E consumer
public void popAll(Collection<? super E> dst) {
while (!isEmpty())
dst.add(pop());
}
ここでの教訓は明確で、インプットされる引数が、producerまたは、consumerのどちらかの役割を果たすときワイルドカードを使う、ということである。
インプットされる引数がproducer、consumerのどちらの役割も果たす場合には、ワイルドカードを使う必要はない。
使いどころは以下のように記憶する。
PECS stands for producer-extends, consumer-super.
これはつまり、型変数Tがproducerである場合には、<? extends T>
を用い、consumerである場合には<? super T>
を用いるということを表している。上のStackの例もそのようになっている。
このPECSの具体例を本章の例に適用していく。
Item28の以下のコンストラクタについて考える。
public Chooser(Collection<T> choices)
この引数はproducerの役割を果たすので、PECSに従い、以下のようになる。
// Wildcard type for parameter that serves as an T producer
public Chooser(Collection<? extends T> choices)
次に、Item30の以下のunionメソッドについて考える。
public static <E> Set<E> union(Set<E> s1, Set<E> s2)
引数はどちらもproducerの役割を担うため、以下のようになる。
public static <E> Set<E> union(Set<? extends E> s1,
Set<? extends E> s2)
ここでの注意点は、戻り値の型には境界ワイルドカードを使ってはならないということだ。
そのような対応は、ユーザにとって柔軟性が増すというより、クライアントのコードの中でワイルドカードを使うことを強いることになる。
クラスの使用に当たって、ユーザが境界ワイルドカードのことを考えないといけないのは、そのAPIに問題があると考えられる。
Java8より前だと以下のように型を明示してやらねばならない。
// Explicit type parameter - required prior to Java 8
Set<Number> numbers = Union.<Number>union(integers, doubles);
次に、Item30のmaxメソッドについて考える。
public static <T extends Comparable<T>> T max(List<T> list)
これをPECSに当てはめると、以下のようになる。
public static <T extends Comparable<? super T>> T max(
List<? extends T> list)
ここでは2回PECSを当てはめている。
Comparableは常にconsumerなので、Comparable<T>
よりComparable<? super T>
を選択するべき。
Comparatorも同様なので、Comparator<T>
よりComparator<? super T>
を選択するべき。
そのほかに、ワイルドカード関連で議論すべきは、型パラメータを使うべきか、ワイルドカードを使うべきかの問題がある。
例えば、listのスワップを行うメソッドは、型パラメータを使用した場合と、ワイルドカードを使用した場合の2通りが考えられる。
// Two possible declarations for the swap method
public static <E> void swap(List<E> list, int i, int j);
public static void swap(List<?> list, int i, int j);
どちらが好まれるかというと、2つ目のワイルドカードを使用したメソッドの方がシンプルであるので好まれる。
一般に型変数がメソッド内の一か所にしか現れない場合には、ワイルドカードに置き換えるべきである。
2つ目のメソッドは以下のように、それだけを書くとコンパイルエラーとなる。
public static void swap(List<?> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
型 List<capture#2-of ?> のメソッド set(int, capture#2-of ?) は引数 (int, capture#3-of ?) に適用できません
これを解消するためには、ワイルドカードを型にはめるヘルパークラスを作成する。
package tryAny.effectiveJava;
import java.util.List;
public class GenericesTest9 {
public static void swap(List<?> list, int i, int j) {
swapHelper(list, i, j);
}
// Private helper method for wildcard capture
private static <E> void swapHelper(List<E> list, int i, int j) {
list.set(i, list.set(j, list.get(i)));
}
}
こうすることによって、実装が多少複雑になるが、ユーザにはシンプルなswapメソッドを見せることができる。