Javaジェネリクス入門、その6

2019年6月4日

Javaジェネリクス入門、その5の続きである。JavaジェネリクスについてはJavaジェネリクスにまとめがあるので参照されたい。

今回は共変を説明するのだが、もし完全に理解できる自信があるならば、この回は読まずにJavaジェネリクス:共変、反変、非変(これ以上簡単にはならない)を読んで欲しい。ここでは共変に加えて、反変について説明している。

共変の分かりづらさ

これはオブジェクト指向言語ではごく普通のことなのだが、以下が可能だ。

犬は動物であり、人間は動物である。であれば、両方とも動物として扱える

class Animal {}
class Dog extends Animal {}
class Human extends Animal {}

とすると、

Dog taro = new Dog();
Human hanako = new Human();
Animal a0 = taro;
Animal a1 = hanako;

ということになる。これはどんなオブジェクト指向言語でも同じ概念だ。

しかし、これに反して、直感的にはわかりづらいのだが、

犬のリストや人間のリストは、動物リストとして扱えない。

どういうことかと言えば。。。

List<Dog>dogList = new ArrayList<>();
List<Human>humanList = new ArrayList<>();

List<Animal>a0List = dogList; // ダメ!エラーになる
List<Animal>a1List = humanList; // ダメ!エラーになる

これを共変ではないと言うらしい。しかし、こんな言葉を覚える必要はない。そう呼ばれているだけの話だ。

もし共変だったらどうなる?

なぜ共変でないのかと言えば、もしこれを許してしまえば(共変であるとすると)、以下ができてしまう。

// 犬リストを作成する
List<Dog>dogList = new ArrayList<>();

// 動物リストとして扱う
List<Animal>a0List = dogList; // 本来はエラー

// 人間が入ってしまう!
a0List.add(new Human());

本来は「犬だけのリスト」のはずだったのに、動物リストとして扱うことによって、人間が格納できてしまうのだ。これはまずい。

「共変」できないと不便極まりない

しかし、どうしても犬リストや人間リストを「動物リスト」として扱いたい場合もある。

もしそれができないとすれば、何かしら「動物リストに共通する処理」を使うことはできず、犬リスト専用、人間リスト専用の処理をわざわざ作らなければならなくなる。

// 動物リスト共通の処理。。。だが、犬リストや人間リストには使えない!
void processAnimal(List<Animal>list) {
   ....
}

// 犬リスト専用処理を作らなければならない(もちろん人間リスト用も)
void processDog(List<Dog>list) {
  ....
}

共変にする方法

そこで登場するのが「? extends」という記法になる。以下である。

List<Dog>dogList = new ArrayList<>();
List<Human>humanList = new ArrayList<>();

List<? extends Animal>a0List = dogList; // OK
List<? extends Animal>a1List = humanList; // OK

processAnimal(dogList); // OK
processAnimal(humanList); // OK
....

void processAnimal(List<? extends Animal>list) {
  ...
}

つまり、List<? extends Animal>には、Animal以下のDogやHumanのリストを格納することができる。

しかし、ここで思い出して欲しい、そもそも共変が禁止されているのは、犬リストに人間を追加するといった矛盾が発生しないようにするためだったはずだ。

その解決策としては、「? extends」というリストには何も入れられなくすることだ。

List<? extends Animal>a0List = dogList; // これはOKになるが、
a0List.add(new Human()); // これはエラーになる!

....
void processAnimal(List<? extends Animal>list) {
  list.add(new Human()); // これもエラー!
}

つまりこうなる。

  • 「? extends」によって犬リストや人間リストを動物リストとして扱えるようになる。
  • しかし、この動物リストに要素を追加することはできなくなる。
  • ただし、要素の読み出しは可能。当然だが、読み出すと動物として返されるため、動物に対する共通処理はできる。

ということになる。つまりは、犬リストや人間リストを、動物リストとして扱うことができるようになるが、読み出しのみが可能になり、書き込みは不可になる

続きはJavaジェネリクス入門、その7になる。