【Effective Java】項目28:API の柔軟性向上のために境界ワイルドカードを使用する(その2)

今回は「項目28:API の柔軟性向上のために境界ワイルドカードを使用する」の後半。

明示的型パラメータ

前回の記事では境界ワイルドカード型について説明しました。 そして、それを利用することで API の柔軟性が向上することについて述べました。

今回は境界ワイルドカード型を実際に API として利用してみます。 まず、項目27の union メソッドを PECS 原則にしたがって書き換えてみましょう。

union メソッドは以下のように実装されていました。

public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
    Set<E> result = new HashSet<E>(s1);
    result.addAll(s2);
    return result;
}    

この union() において、2つの引数 s1 と s2 は両者とも値を生成しています。 すなわち、プロデューサーとして機能しています。 そのため、PECS 原則 (Producer-Extends and Consumer-Super) に従って、これらの型宣言を <? extends E> に変更します。

戻り値にはワイルドカード型を利用しないでください。 戻り値にワイルドカード型を使うと、クライアントコードではワイルドカード型の変数を宣言する必要があります。 クライアント側でワイルドカード型を意識する必要がある API は多くの場合誤りです。

PECS 原則を適用した union は以下のようになります。

public static <E> Set<E> union(Set<? extends E> s1, Set<? extends E> s2) {
    Set<E> result = new HashSet<E>(s1);
    result.addAll(s2);
    return result;
}    

この新しくなった union を、早速 Set<Integer> と Set<Number> で使ってみます。

Set<Integer> integers = new HashSet<>();
Set<Double> doubles = new HashSet<>();
Set<Number> numbers = union(integers, doubles);
// => コンパイルエラー。

ジェネリックスの理論上、このコードは問題ないはずですが、実際にはコンパイルエラーが発生してしまいます。これは、Java の仕様における型推論の制限事項です。

この仕様上の問題を回避するために、ジェネリックメソッドに明示的な型パラメータを与える必要があります。 明示的型パラメータは Class.<Parameter>methodName() のように書きます。 <Parameter> の部分で明示的に型パラメータを指定しています。

Set<Number> numbers = Union.<Numebr>union(integers, doubles);
// => 明示的型パラメータ指定によるコンパイルエラーの回避
// なお、ここで Union は union メソッドが含まれるクラスを示しています。

明示的型パラメータは、コードの冗長性を増すのでなるべく書かないべきです。 実際には、ほとんどの場合は明示的型パラメータを利用する必要はありません。

Comparable に対する PECS

次に、項目27で触れたリストから最大値をとりだす max() に PECS を適用してみます。 オリジナルの max は以下のように実装されています。

public static <T extends Comparable<T>> T max(List<T> list) {
    Iterator<T> it = list.iterator();
    T result = it.next();
    while (it.hasNext()) {
        T t = it.next();
        if (t.compareTo(result) > 0) {
            result = t;
        }
    }
    return result;
}

まず、メソッドの引数である List<T> list は、メソッド内で値を生成し、ジェネレーターとして機能しているので、引数は List<? extends T> に変更することができます。

この際、メソッド内の Iterator<T> it を Iterator<? extends T> it に変更する必要があります。

変更後の max() は以下のようになります。

public static <T extends Comparable<T>> T max(List<? extends T> list) {
    Iterator<? extends T> it = list.iterator();
    ... (省略) ...
}

次に戻り値の再帰型境界の Comparable に関して PECS を適用してみましょう。

<T extends Comparable<T>> は「自分と比較できる何らかの型」を示していました。 ここで「比較できる」というのは実装上、「T が Comparable のメソッド int compareTo(T value) を実装している」という意味になります。

すなわち、T は compareTo(T value) によって値を消費するコンシューマーであると考えることができます。 そこで、Comparable は Comparable<? super T> とするべきです。

最終的なメソッドは以下のようになります。

public static <T extends Comparable<? super T>> T max(List<? extends T> list) {
    ... (省略) ...
}

これは、Effective Java 内でも最も複雑なメソッド宣言です。

実際の例として java.concurrent の ScheduleFutures は、Comparable<ScheduleFutures> を実装しておらず、自分自身と直接の比較はできません。 ScehduleFutures は Comparalbe<Delay> を実装しており、そのような場合にこの max は正しく機能するようになります。

メソッド宣言とキャプチャ

ジェネリックスを使ったメソッド宣言では、メソッド宣言を書く方法が二種類あります。 型パラメータを用いた方法とワイルドカードを用いた方法です。

もし、型パラメータが宣言中に一度しか現れないのならば、ワイルドカードを利用するべきです。 この方が単純に文字数が少なく、分かりやすいからです。

// 型パラメータを用いたメソッド宣言
static <E> void swap(List<E> list, int i, int j);

// ワイルドカードを用いたメソッド宣言
// ただし、型パラメータが複数個登場する場合には利用できない。
static void swap(List<?> list, int i, int j);

ワイルドカード型のメソッド宣言を利用できる場合には、そちらを利用するべきなのですが、ワイルドカードを用いたメソッド宣言を実装するためには「キャプチャ」と呼ばれるテクニックが必要になります。

実際に、ワイルドカード型のメソッド宣言に対する以下の実装はコンパイルされません。 これは、List<?> には null 以外の値は挿入できないからです。

static void swap(List<?> list, int i, int j) {
    list.set(i, list.set(j, list.get(i)));
    // => コンパイルエラー
    // List<?> には null 以外の値はいれられない
}

ここでキャプチャーメソッドが必要となります。 キャプチャーメソッドワイルドカード型の実際の型を補足するため、ジェネリックメソッドです。

// ワイルドカードキャプチャのための private ヘルパーメソッド
// このメソッドの型パラメータ宣言によって型を補足する
private static <E> void swapHelper(List<E> list,  int i, int j) {
    list.set(i, list.set(j, list.get(i)));
}

static void swap(List<?> list, int i, int j) {
    swapHelper(list, i, j);
}

このように、外部に公開するメソッドに関してはワイルドカード型の宣言を用いて、内部的にはキャプチャメソッドを使って型安全性を保つ、という方法がジェネリックメソッド API の定石です。

感想

最後のキャプチャに関する説明がよく分からなかった。

ワイルドカードを公開することがどれほどのメリットとなるのか? あと、swapHelper() に list をいれられるということは、List<E> に List<?> 型を代入できるということか?

キャプチャについてはもう少し調べてちゃんとまとめる必要がありそうだ。