今回は「項目28:API の柔軟性向上のために境界ワイルドカードを使用する」。
項目28はけっこう長いので2回に分割した。
不変性による弊害
項目25で説明したように Java のジェネリックスは不変です。 Object は Number のスーパータイプですが、List<Object> は List<Number> のスーパータイプではありません。
これは型安全性を保つために必要な性質である一方、この性質によって API の柔軟性が損なわれる場合があります。
例えば、push() と pop() を持つ典型的なスタックを考えてみます。
public class Stack<E> { public Stack() {} public void push(E element) { ... } public E pop() { ... } public boolean isEmpty() { ... } }
このジェネリックス Stack を利用してみます。 Integer インスタンスを push するコードは以下のようになります(Number は Integer のスーパータイプです)。
Stack<Number> numbers = new Stack<>(); numbers.push(new Integer(1)); // => 問題なし(分かりやすさのために明示的にボクシングしています)
次に、このスタックに対して、「すべての要素を追加する」ことのできるメソッド pushAll() を追加したいと考えてみます。 おそらく実装は以下のようになるはずです。
public void pushAll(List<E> src) { for (E e: src) { this.push(e); } }
ここで問題が発生します。 単一の Integer インスタンスは問題なく push できる一方、List<Integer> は、コンパイルエラーが発生するため pushAll できません。
List<Integer> integers = ...; numbers.pushAll(integers); // => コンパイルエラー // List<Number>(pushAll の 仮引数の型)は List<Integer> のスーパータイプではないため。 // もし、Java のジェネリックスが共変であればコンパイル可能
この制限はジェネリックスの不変性に起因するものですが、スタックの API としては非常に不便です。
次に、pop の場合について考えてみます。 pop した値を Object 型の obj 変数に格納するコードは以下のようになります(Object は Number のスーパータイプです)。
Stack<Number> numbers = new Stack<>(); numbers.add("1"); Object obj = numbers.pop(); // => 問題なし
これに対して、渡したリストにすべての要素を追加してくれる popAll() を実装したいと考えてみます。
public void popAll(List<E> dst) { while (!isEmpty()) { dst.add(pop()); } }
ここで再び問題が発生します。 単一の要素の場合、pop して Object の変数に代入できる一方、List<Object> を popAll に渡すことができません。 コンパイルエラーが発生してしまいます。
Stack<Number> numbers = new Stack<>(); numbers.add("1"); numbers.add("2"); List<Object> objects = ...; numbers.popAll(objects); // => コンパイルエラー // List<Number>(popAll の 仮引数の型)は List<Object> のスーパータイプではない // もし、Java のジェネリックスが反変であればコンパイル可能
pushAll と popAll の例に見るように、ジェネリックスの不変性によって、ジェネリックスを引数にとる API が不便にならざるを得なくなってしまいました。
境界ワイルドカード型
Java ではこれらの不便さを解消するために『境界ワイルドカード型』と呼ばれる特殊なパラメータ型を提供しています。
上で述べた pushAll の引数の型は List<E> でしたが、API の意図としてここで宣言したい型は「E のリスト」ではなく、「E のサブタイプをパラメータとするリスト」のはずです。
これを意味する型として、境界ワイルドカード型を用いて List<? extends E>
と書くことができます。
public void pushAll(List<? extends E> src) { // => 境界ワイルドカード型を利用した引数宣言 for (E e: src) { this.push(e); } }
次に popAll について考えてみます。 上で述べた popAll の引数の型は List<E> でしたが、API の意図としてここで宣言したい型は「E のリスト」ではなく、「E のスーパータイプをパラメータとするリスト」のはずです。
それを意味する型として、先ほどの extends ではなく super というキーワードを使って、List<? super E> と書くことができます。
public void popAll(List<? super E> dst) { while (!isEmpty()) { dst.add(pop()); } }
これによって、さきほどはコンパイル出来なかったコードがコンパイルできるようになり、実行できるようになります。
Stack<Number> numbers = new Stack<>(); List<Integer> integers = ...; numbers.pushAll(integers); List<Object> objects = new ArrayList<>(); numbers.popAll(objects); // => すべてコンパイル可能で、実行時に例外も発生しない。
Java のジェネリックスは不変であるため、型パラメータにスーパータイプ/サブタイプ関係があったとしてもジェネリックス型自体にその関係はありませんでした。 境界ワイルドカード型を使うことでこの制限を取り払うことができる、と考えることもできます。
このように、境界ワイルドカード型を使って、型安全性を確保しつつ API は柔軟性を高めることができるのです。
GET & PUT 原則(PECS)
境界ワイルドカード型に関して、<? extends E> と <? super E> のどちらを使うかを決めるための原則があります。
関数内において、ジェネリックス型の引数の役割が「プロデューサー(Producer)」 であれば extends を、「コンシューマー(Consumer)」であれば super を用います。 これが Get & Put 原則と呼ばれる原則です。
プロデューサーとは関数内で、何らかの値を生成(提供)する引数のことです。 pushAll の例では、引数の src 変数は push する対象となる値を生成するため、プロデューサーの役割を担っていました。
一方、コンシューマーとは関数内で、何らかの値を消費(利用)する引数です。 popAll の例では、引数の dst 変数は pop された値を消費する役割であるため、まさにコンシューマーでした。
この原則は Producer-Extends and Consumer-Super を略して「PECS」とも呼ばれます。
次回へ続く
次回は、この Put & Get 原則の適用例やキャプチャについて説明します。
感想
ジェネリックスも佳境に入ってきたな〜。