ジェネリック 境界ワイルドカード型の整理

境界ワイルドカードは他のプログラミング言語には多分存在しない。 ただこれがあることで、メソッド間でデータのやり取りが楽になる。

役割

Javaジェネリックは、型パラメーターが継承関係であっても異なる型になる。

つまり List<Number>List<Integer> は型が異なり、メソッドの引数に対して指定することができない。 (Swiftでは問題なく渡すことが出来る)

void receiveNumbers(List<Integer> Integers) { .. };

List<Number> integers = new ArrayList<Integer>();
// 型が異なるので渡せない
reciveNumbers(integers);

そこで 型パラメーターをある程度幅のある指定が出来るようにした のが、境界ワイルドカード

(従って型制約とは全然違うことに注意)

// 「? は Integerのsuperクラスである」と読める
void receiveNumbers(List<? super Integer> numbers) { .. };

List<Number> integers = new ArrayList<Integer>();
// Number は Integerのスーパークラスなので渡すことが可能
reciveNumbers(integers);

種類

境界ワイルドカード型は2種類存在する

  • Producerタイプ - 値の取得のみ可能
  • Comsumertタイプ - 値の書き込みが可能、取得はObject型としてのみ。

以下のFruitクラスを例に整理する

f:id:yossan2:20190908155154p:plain

Fruit型のポイントは以下

  • JavaのクラスはObject型を継承していること
  • Fruit型は2系統あること
    • Melon 系列
    • Lemon 系列

Producerタイプ Generics<? extends T>

f:id:yossan2:20190908155201p:plain

? extends Fruit は、以下の4つの型を表せる

  • Fruit
  • Melon
  • WaterMelon
  • Lemon

fruits への代入

1. Fruit<? extends Fruit> fruits = new ArrayList<Fruit>();
2. Fruit<? extends Fruit> fruits = new ArrayList<Melon>();
3. Fruit<? extends Fruit> fruits = new ArrayList<WaterLemon>();
4. Fruit<? extends Fruit> fruits = new ArrayList<Lemon>();

ワイルドカードは Fruit クラスを継承していることだけが確定しているが、Melon であるのか Lemon であるのかサブクラスは特定できない。

従って、Fruit 型として取得することが可能であるが、直接サブクラスを指定することはできない。

// OK
Fruit fruit = fruits.get(i);

// NG  fruits = new ArrayList<Lemon>(); の可能性があるため矛盾する
Melon melon = fruits.get(i);

// NG  fruits = new ArrayList<Melon>();の可能性があるため矛盾する
Lemon lemon = fruits.get(i);

Producerへ書き込めない理由

List<? extends Fruit> fruits

一見、Fruit型オブジェクトは書き込むことができそうだが、実際の要素の型は確定できないため、以下の矛盾が発生する。

// fruits は Melonのリスト
 Fruit<? extends Fruit> fruits = new ArrayList<Melon>();

// LemonクラスはFruitのサブクラス
Fruit lemon = new Lemon();

// もしProducerへの書き込みができたならば、Lemonインスタンスを追加できてしまう
fruits.add(lemon); 

従ってfruitsのサブタイプのクラスが確定しないため、Producerには書き込むことができない。

Consumerタイプ Generic<? super T>

f:id:yossan2:20190908170949p:plain

fruits への代入

1. fruits = new ArrayList<Melon>();
2. fruits = new ArrayList<Fruit>();

ワイルドカードは、Melonのスーパークラスであることは確定しているので、Melon系列の 要素を書き込むことが可能

fruits.add(new Melon()); // OK
fruits.add(new WaterMelon()); // OK

一見、

2. fruits = new ArrayList<Fruit>();

からfruitsには Fruitオブジェクトを書き込めるように思えるが、以下のように矛盾が発生する。

// fruits の要素は Melonクラスを祖先とするクラスである
Fruit<? super Melon> fruits = new ArrayList<Fruit>();
// LemonはFruitのサブクラス
Fruit fruit = new Lemon();
// Fruitが追加できるようにすると矛盾が発生する
fruits.add(fruit);

従って書き込めるのはMelonクラスを継承しているオブジェクトのみとなる。

Object型としか読み込めない理由

 Fruit<? super Melon> fruits 

fruits の要素の型は Melonクラスの先祖のどれかということになるので、唯一 Object型であることのみ確定している。 従ってObject型としては読み込むことが可能

Object object = fruits.get(i); // OK
Melon melon = fruits.get(i); // NG FruitクラスのサブクラスであるLemonかもしれない