放課後プログラミング

調べたことや考えたことなどを忘れないために書きます。

ワイルドカードの使い方 (ジェネリクスの使い方 3)

以前の投稿「ジェネリクスの使い方 1」「ジェネリクスの使い方 2」で大まかにジェネリクスについてまとめましたが、1,2の投稿だけではワイルドカードを正しく効果的に使うのは難しいと感じました。なぜならワイルドカードの利点について言及できていなかったからです。
今回の投稿ではワイルドカードについて更に掘り下げて、これを使うと何が嬉しくて、いつ使えば良いのかに焦点を当てます。

ワイルドカードでできることのまとめ

下記にワイルドカードでできる操作をまとめました、それぞれの代入操作で左右オペランドの型がどのような制限のある型なのかを意識して見ると理解しやすいと思います。

//準備
class A {}
class B extends A {}
class C extends B {}

List<A> listA = new ArrayList<>();
List<B> listB = new ArrayList<>();
List<C> listC = new ArrayList<>();
List<Object> listO = new ArrayList<>();
A a;
B b;
C c;
Object o;


//非境界ワイルドカード型まとめ
List<?> unbounded;

unbounded = listA; // OK // ワイルドカードに境界がないため、
unbounded = listB; // OK // どのような型に対応付けされたList型(及びListのサブクラス型)でも
unbounded = listC; // OK // 代入可能です。
unbounded = listO; // OK //

listA = unbounded; // NG // 特定の型に対応付けされた総称型に、
listB = unbounded; // NG // 未知の型を持てるにワイルドカード型の総称型を代入することはできません。
listC = unbounded; // NG // また、List<A>型が代入されたList<?>型を再度List<A>型に代入することもできません。
listO = unbounded; // NG // List<?>型に入った時点で<A>への対応付け情報が消えるためです。
                         // 全ての型が継承しているObject型でも代入できないのは、総称型の非変性のためです。

a = unbounded.get(0); // NG // 未知の型の値をA型変数に入れることはできません。
b = unbounded.get(0); // NG // こちらも同様。
c = unbounded.get(0); // NG // こちらも同様。
o = unbounded.get(0); // OK // 全てのオブジェクトはObject型を継承しているため、
                            // Object型の変数には未知の型でも代入可能です。

unbounded.add(a);    // NG // Listにおける非境界ワイルドカード型の場合、
unbounded.add(b);    // NG // セットされる総称型変数に対応付けされた型が完全に不明なため、
unbounded.add(c);    // NG // どのような型でもセットすることはできません。
unbounded.add(o);    // NG // 抽象的に言えば、特定の型に対応付けされた型の変数を、
                           // 非境界ワイルドカード型の変数に代入することはできません。
unbounded.add(null); // OK // ただし、nullに限って境界の有無に関わらず常にセット可能です。


//上限境界ワイルドカード型
List<? extends B> upperBounded;

upperBounded = listA; // NG // 上限境界とは、当該クラスとそのサブクラスのいずれかに対応付けされた
upperBounded = listB; // OK // 総称型のみ代入可能という制限のことをいいます。
upperBounded = listC; // OK // そのため、Bクラスとそのサブクラス以外ではエラーとなります。
upperBounded = listO; // NG //

listA = upperBounded; // NG // 制限が加わったため、listBやそのスーパークラス型の変数には代入可能としても良く見えますが、
listB = upperBounded; // NG // やはり総称型の非変性のため、どのような型に対応付けされた総称型変数にも
listC = upperBounded; // NG // 代入はできません。
listO = upperBounded; // NG //

a = upperBounded.get(0); // OK // 上限境界があるため、ワイルドカードに入りうる型は非境界の場合よりも
b = upperBounded.get(0); // OK // 範囲が狭まっています。この場合では、B型とそのサブクラス型しか入らないため、
c = upperBounded.get(0); // NG // B型とそののスーパークラスには全て代入可能です。
o = upperBounded.get(0); // OK // これらはB b; A a = b;とできて、B b; C c = b;とできないことと全く同じ意味です。

upperBounded.add(a);    // NG // <? extends B>という上限境界があるとき、?はBとそのサブクラスに対応付けされる
upperBounded.add(b);    // NG // ことが許容されますが、それが実際にはC型に対応付けされているのか、あるいは
upperBounded.add(c);    // NG // 別のサブクラスやもっと深いサブクラスが存在して、それらに対応付けされているのかはわかりません。
upperBounded.add(o);    // NG // そのため、B型やそのサブクラス型の変数であってもセットすることはできません。
upperBounded.add(null); // OK // すなわち、特定の型を下限が未知の型に代入することはできません。


//下限境界ワイルドカード型
List<? super B> lowerBounded;

lowerBounded = listA; // OK // 下限境界とは、当該クラスとそのスーパークラスのいずれかに対応付けされた
lowerBounded = listB; // OK // 総称型のみ代入可能という制限のことをいいます。
lowerBounded = listC; // NG // そのため、BのサブクラスとなるCに対応付けされた総称型の変数は
lowerBounded = listO; // OK // 代入することができません。
                            // ObjectクラスからBクラスまでの直系に存在しないクラス型の変数も代入できません。

listA = lowerBounded; // NG // 今まで説明してきたとおり、総称型の非変性により、これらは常に代入できません。
listB = lowerBounded; // NG //
listC = lowerBounded; // NG //
listO = lowerBounded; // NG //

a = lowerBounded.get(0); // NG // <? super B>という下限境界により、Bとそのスーパークラス型に
b = lowerBounded.get(0); // NG // 対応付けされた総称型のみ許容されるため、それが実際にはB型なのか
c = lowerBounded.get(0); // NG // A型なのかあるいはもっと上位のクラス型なのかわからないため、
o = lowerBounded.get(0); // OK // 全てのオブジェクトが継承しているObject型にのみ代入が可能です。

lowerBounded.add(a);    // NG // 下限境界により、Bとそのスーパークラス型に対応付けされた総称型であるとわかるため、
lowerBounded.add(b);    // OK // Bとそのサブクラス型の変数は全てセットすることができます。
lowerBounded.add(c);    // OK // これらも、B b; A a = b;とできて、
lowerBounded.add(o);    // NG // B b; C c = b;とできないことと全く同じ意味です。
lowerBounded.add(null); // OK //


//ワイルドカードが使える場所と使えない場所
class A<?>{}                 // NG // クラス宣言の型
class A extends B<?>{}       // NG // クラス宣言のスーパータイプの型
class A extends B<List<?>>{} // OK // クラス宣言のスーパータイプの型の型
? method(){}                 // NG // メソッドの返り値型
List<?> method(){}           // OK // メソッドの返り値型の型
void method(? x){}           // NG // 変数宣言の型
void method(List<?> list){}  // OK // 変数宣言の型の型
? x;                         // NG // 変数宣言の型
List<?> list;                // OK // 変数宣言の型の型
new ArrayList<?>();          // NG // インスタンス生成の型
new ArrayList<List<?>>();    // OK // インスタンス生成の型の型
//基本的にそこに書ける最上位の型としてワイルドカードは使えない
PECS - (Producer - extends, Consumer super)

上記のまとめはPECSを示しています。
すなわち、Producer(生産者)はextendsを使い、Consumer(消費者)はsuperを使いましょうという意味です。
生産者はgetされるオブジェクトで、消費者は何かをsetするオブジェクトです。
これは、extends Xを使ってXを上限境界に設定すれば、getした結果をX以上の型に入れられ、(生産し、提供できる)
super Xを使ってXを下限境界に設定すれば、X以下の型なら自由にset(消費行動)できるためです。

いつ使えばいいのか(1)

ではどのような時に使うと「ワイルドカード機能があってよかった」と思えるのでしょうか。
producerリストの内容をconsumerリストにディープコピーする2つの同じ動作をするメソッドを見比べてみると、ワイルドカード機能は不要に見えます。

<T> void copy(List<? extends T> producer, List<T> consumer){
    consumer.clear();
    for (T t : producer){
        consumer.add(t);
    }
}

<T, T2 extends T> void copy(List<T2> producer, List<T> consumer){
    consumer.clear();
    for (T t : producer){
        consumer.add(t);
    }
}

オラクルの公式ドキュメント曰く、ワイルドカードを用いた方が簡潔なコードとなるため、別の場所で参照されない型は型パラメータを使わずにワイルドカードを用いた方が良いとしています。
上記の例で言えば、後者メソッドの型パラメータT2はわざわざパラメータ化しているにも関わらずどこからも使われておらず、たんにTのサブクラス型の何かという情報を持たせたいだけなので、前者メソッドの方法で十分なのです。
また、万が一下記のようなメソッドを実装したい場合はワイルドカードを使わざるを得ないかと思います。

public List<?> getList(boolean b){
    return b
           ? new ArrayList<String>(){{ add("a"); }}
           : new ArrayList<Integer>(){{ add(1); }};
}

// 普通はもっと上位ロジックの部分で分岐させた方が他のロジックも書きやすいのではないかと思います。
// なぜならList<?>を返り値として受けてもそのリストに対してnull以外setすることができず、
// オブジェクト型としてしかgetできないからです。(要するに上記メソッドは不便なだけ)
if(b){
    List<String> list = new ArrayList<String>(){{ add("a"); }};
    ... //listに行う処理
} else {
    List<Integer> list = new ArrayList<Integer>(){{ add(1); }};
    ... //listに行う処理
}

いつ使えばいいのか(2)

上記で述べた利点のためだけにワイルドカード機能が追加されたわけではありません。
ここまでは全てジェネリクスがサポートされたコードを前提として解説してきましたが、ワイルドカードが本質的に力を発揮するのはジェネリクスがサポートされる以前のコードをジェネリクスを使ったコードに書き換える時です。この作業は慎重に行わないと以前と比べて制限が厳しくなってしまう場合があります。

たとえばここで、CollectionインタフェースのcontainsAll()について見てみます。

// ジェネリクスサポート前
interface Collection {
    public boolean containsAll(Collection c);
}

//ジェネリクスサポート後
interface Collection<E> {
    public boolean containsAll(Collection<?> c);
}

ここで、なぜジェネリクスサポート後のcontainsAll()メソッド宣言は

public boolean containsAll(Collection<E> c);

ではないのでしょうか?
理由はジェネリクスサポート前の制限との整合性を保つためです。
containsAll()AbstractCollectionで下記のように実装されて、ArrayListに継承されています。

public abstract class AbstractCollection<E> implements Collection<E> {
    public boolean containsAll(Collection<?> c) {
        for (Object e : c)
            if (!contains(e))
                return false;
        return true;
    }
}
List list1 = new ArrayList(){{ add(1); add(2); }};
List list2 = new ArrayList(){{ add("a"); add("b"); }};
System.out.println(list1.containsAll(list2)); // -> false
List<Integer> list1 = new ArrayList<Integer>(){{ add(1); add(2); }};
List<String> list2 = new ArrayList<String>(){{ add("a"); add("b"); }};
System.out.println(list1.containsAll(list2)); // -> false

このように、ジェネリクスを用いない実装とジェネリクスを用いた実装で整合性が保たれています。
次に、以下のように<E>を使ったbadContainsAll()を実装し、それを使ってみるとどうでしょうか。

class BadList<E> extends ArrayList<E> {
    public boolean badContainsAll(Collection<E> c){
        for (Object e : c)
            if (!contains(e))
                return false;
        return true;
    }
}
BadList list1 = new BadList(){{ add(1); add(2); }};
BadList list2 = new BadList(){{ add("a"); add("b"); }};
System.out.println(list1.badContainsAll(list2)); // -> false
BadList<Integer> list1 = new BadList<Integer>(){{ add(1); add(2); }};
BadList<String> list2 = new BadList<String>(){{ add("a"); add("b"); }};
System.out.println(list1.badContainsAll(list2)); // -> コンパイルエラー

// Error: java: クラス BadList<E>のメソッド badContainsAllは指定された型に適用できません。
//   期待値: java.util.Collection<java.lang.Integer>
//   検出値: BadList<java.lang.String>
//   理由: 実引数BadList<java.lang.String>はメソッド呼出変換によってjava.util.Collection<java.lang.Integer>に変換できません

型安全という意味では正しい挙動に見えますが、ジェネリクスを用いない実装とジェネリクスを用いた実装で動作に違いが出ています。

まとめ

完全に新規のプロダクトをジェネリクスサポート後のjavaで実装する場合はワイルドカードが大活躍する場面はあまりありませんが、ジェネリクスサポート前のプロダクトをジェネリクスで書き換える際には必要に応じてワイルドカードを使うことになります。
たとえばjava.util.Collections.max()メソッドは、このように宣言されています。

public static <T extends Object & Comparable<? super T>> T max(Collection<? extends T> coll) {
    ...
}

型パラメータがかなり複雑に利用されています。
ジェネリクスサポート前の同メソッドはこのように宣言されています。

public static Object max(Collection coll) {
    ...
}

本来であれば

public static <T extends Comparable<? super T>> T max(Collection<T> coll) {
    ....
}

とするのが自然に思えますが、ジェネリクスサポート前のmax()の実装に挙動を合わせるために、
1. ジェネリクスサポート前の引数型がCollectionのため、中身に複数クラスのデータが入る可能性があり、<? extends T>としてそのようなデータも受け入れられるようにしています。
2. ジェネリクスサポート前の返り値型がObject型のため、上限境界を引き上げるためにextends Objectを加えています。

このように古い実装を新しい実装に書き換える際に、非変な型パラメータを用いたジェネリクスだけでは対応できない部分があるため、ワイルドカード機能が実装されています。