境界ワイルドカード型の役割
Javaのジェネリクス型は、List<Integer>
型を List<Number>
型に変換することは出来ません。
// OK 型パラメーターが同一の場合
List<Integer> foo = new ArrayList<Integer>();
// NG List<Integer> は、List<Number> に変換できない
List<Number> bar = foo;
// NG 初期化でも同様に変換不可
List<Number> foo = new ArrayList<Integer>();
そこで境界ワイルドカード型を使うことで、型パラメーターをある程度幅を持たせて宣言する ことが出来ます。
しかしながら境界ワイルドカード型には2種類存在します。
これは型の境界を定めた時に、それぞれある場面において処理系が「型を定められない」という問題が発生するためです。
ひとつひとつその問題を見ていこうと思います。
解説には以下のFruitクラスを用います。
上限付きワイルドカード型
Generic<? extends T>
型パラメーターを上記のように指定するとその型パラメーターは 上限付きワイルドカード型 です。
? extends T 型は、Tもしくはそのサブクラスを全て表す型 を意味します。
List<? extends Fruit>
型を使って考えると、Fruitクラスもしくはそのサブクラスを指定したList型 から変換が可能です。
List<? extends Fruit> basket = new ArrayList<Fruit>(); // 1
List<? extends Fruit> basket = new ArrayList<Melon>(); // 2
List<? extends Fruit> basket = new ArrayList<Lemon>(); // 3
次に、List<? extends Fruit> basket
変数から要素を取り出すことを考えますが、
ここでbasket変数は、1のときも2のときも3のときも、要素をFruitクラスとして取得が可能なことを認識しておいてください。
読み込み
「上限付き境界ワイルドカード型」から「Tクラス型」への変換
List<? extends Fruit> basket
変数から要素を取り出すことを考えます。
上限付きワイルドカード型の場合、 少なくともTクラスとそのサブクラスを表す型 であるから、Listから要素を取得する際、その上限であるTを指定することが可能です。
List<? extends Fruit> fruits = ...
// 問題なし
Fruit fruit = fruits.get(i);
これは ? extends T
型から T
型への変換になっています。
「上限付き境界ワイルドカード型」から「Tのサブクラス型」への変換
次にbasket変数から要素を取得する際に、そのサブクラスを指定出来るかを考えます。
List<? extends Fruit>
型には少なくとも以下の3つのList型から変換可能です。
ArrayList<Fruit>
ArrayList<Melon>
ArrayList<Lemon>
従って要素を取得する際に、いくつもの系列に派生していくサブクラスを指定することが出来ません。
List<? extends Fruit> basket = ...
// NG コンパイルエラー Melon型には変換できません
Melon fruit = basket.get(i);
上記、もし? extends Fruit
型をサブクラスへと変換出来るとした場合、以下の矛盾が発生する。
// メロンリストを作る
List<Melon> melonList = new ArrayList<Melon>()
melonList.add(new Melon());
// List<Melon> melonList型をList<? extends Fruit>型に変換
List<? extends Fruit> basket = melonList;
// サブクラスへの変換が可能であればLemonとして受け取ることが可能となってしまう
Lemon fruit = basket.get(i);
従ってサブクラスへの変換が許されていません。
よって ? extends T
型から Tのサブクラス
型への変換は不可です。
書き込み
次に List<? extends Fruit> fruits
変数への要素の追加を考えます。
? extends Fruit
型 から Fruit型
への変換は出来るので、以下のfruits変数にFruitオブジェクトを追加出来るように思われます。
List<? extends Fruit> fruits = new ArrayList<Fruit>();
// 一見可能に思える
fruits.add(new Fruit());
しかしながら上記を許してしまうと、以下の矛盾が発生します。
// fruitsの実態はメロンリスト
List<? extends Fruit> fruits = new ArrayList<Melon>();
// LemonクラスはFruitクラスのサブクラス
Fruit fruit = new Lemon();
// メロンリストにレモンが追加出来ることになる
fruits.add(fruit);
従って、具体型であるT
型もしくは Tのサブクラス
型から? extends T
型 への変換は出来ません。
これはList型を上限付きワイルドカードで宣言すると、要素を追加することが出来ないことを意味します。
そこでもう一つの境界ワイルドカード型の出番です。
※ Appendixに具体型から変換するということについて解説を追記しています。
下限付きワイルドカード型
Generic<? super T>
型パラメーターを上記のように指定するとその型パラメーターは、下限付きワイルドカード型 です。
? super T 型は、Tもしくはそのスーパークラス全てを表す型 を意味します。
List<? super Melon> 型には、Fruitクラスもしくはそのスーパークラスを型パラメーターとするList型 から変換出来ます。
List<? super Melon> baskets = new ArrayList<Melon>(); // 1
List<? super Melon> baskets = new ArrayList<Fruit>(); // 2
List<? super Melon> baskets = new ArrayList<Food>(); // 3
List<? super Melon> baskets = new ArrayList<Object>(); // 4
今度は前後して、basket変数へ要素を追加することを先に考えますが、
1~4において、1も2も3も4のときもbasket変数には、Melonクラスが追加可能であることを認識しておいてください。
書き込み
下限境界ワイルドカード型への変換
List<? super Melon> basket
変数へ要素を追加するには、下限付きワイルドカード型の場合、少なくともTクラスもしくはそれ以上のスーパークラスを表す型 であるから、List型へ要素を追加する際、その下限であるTを指定することが出来るはずです。
例を参考に一つ一つ追加出来ることを確認します。
List<? super Melon> fruits = new ArrayList<Object>()
の時
// OK
fruits.add(new Melon())
// OK
fruits.add(new WaterMelon());
List<? super Melon> fruits = new ArrayList<Food>()
の時
// OK
fruits.add(new Melon())
// OK
fruits.add(new WaterMelon());
List<? super Melon> fruits = new ArrayList<Melon>()
の時
// OK
fruits.add(new Melon());
// OK
fruits.add(new WaterMelon());
つまり T
もしくは Tのサブクラス
型から ? super T
型への変換は可能です。
読み込み
? super Melon 型は、「Melonクラスとそのスーパークラスを全て表す型」です。
またJavaのすべてのクラスは、Object 型を継承しています。
従って ? super T
型から Object
型への変換は可能です。
Object object = basket.get(i)
まとめ
-
境界パラメーター型を用いればジェネリクス型の型を幅広く表すことが出来ます
-
上限付き境界ワイルドカード型と具体型の変換表
可否 変換元 変換先 その意味 OK ? extends T
T
T
型の値の取得が可能NG T
? extends T
書き込みが不可 -
下限付き境界ワイルドカード型と具体型の変換表
可否 変換元 変換先 その意味 OK ? super T
Object
Object型として値の取得が可能 OK T
? super T
T型の値の書き込みが可能 OK Tサブクラス
? super T
Tのサブクラス型の値の書き込みが可能 -
一般的に PECS という呪文が存在し、上限付き境界ワイルドカード型をProducerと呼び、下限付き境界ワイルドカード型をConsumerと呼ぶことがあります
- Producer - 値の生成専門
- Consumer - 値の受取専門
Appendix
境界ワイルドカード型による値の書き込みが意味すること
上限付き境界ワイルドカード型を指定した時、そのListは書き込むことが出来ないということを耳にしたことがあるかもしれません。
これは単純に以下が出来ないという意味です。
具体型
から 上限付きワイルドカード型
への変換が出来ない
ジェネリクス型オブジェクトを上限付きワイルドカードを用いて宣言するとその型パラメーターを持つ関数を呼び出した際に上記の変換になってしまいます。
以下Stack<T> クラスを考えます。
class Stack<T> {
void push(T e) {}
}
Stack<T>クラスを上限付きワイルドカード型を用いて宣言します。
Stack<? extends Fruit> basket = new Stack<Fruit>();
この時basket変数の型パラメーターTは ? extends Fruit
型です。
つまりpushメソッドは以下と同等です。
void push(`? extends Fruit` e) {}
従ってこのpushメソッドを呼び出すと、具体型(Fruit)
から ? extends Fruit
型への変換になります。
そしてこの変換はNGです。
// NG Fruit型を `? extends Fruit` 型へは変換できない
basket.push(new Fruit());