Javaジェネリクス:共変、反変、非変(これ以上簡単にはならない)
Javaのジェネリクスの共変、反変、非変について書いてみるのだが、非常に重要な概念であるにも関わらず、ネットを検索してみると、非常に理屈っぽく難しい説明しか無いように思う。
これらを読んでみると、「俺はバカなのか」と感じること請け合いなのだが、しかしあなたのせいではない。彼らは「わざと難しそうに書いている」だけである。
ここでは実用上の観点から思い切りわかりやすく書いてみることにする(成功しているかどうかはわからないが)
※もしこのページで理解できないならば、以下を参照してほしい。
前提
以下では、クラス階層として次のようなものを用いる。
Animal
+-- Dog
+-- Cat
以下のようなクラスになる。
public static class Animal {}
public static class Dog extends Animal {}
public static class Cat extends Animal {}
問題点
以下は当然可能だ。
List<Dog>dogList = new ArrayList<Dog>();
型パラメータが同じなので、単純にArrayListはその上位のクラス(この場合はインターフェースだが)として扱うことができる。
しかし、以下はできない。
List<Animal>animalList = new ArrayList<Dog>();
DogはAnimalのサブクラスであり、ArrayListはListのサブクラスなのにできないのである。なぜだろうか?
もしこれを許してしまうと、以下ができてしまう。
// 犬のリストを
List<Dog>dogList = new ArrayList<Dog>();
// 動物リストに変換
List<Animal>animalList = dogList; // 本来はエラー
// 猫が入ってしまう!
animalList.add(new Cat());
犬のリストのはずなのに、動物リストとして扱うことによって、猫が格納できてしまうのである。これではいかにもまずい。
禁止されている(コンパイラエラーになる)のは、この理由のためだ。
これを難しい言葉で非変(invariant)というらしい。
ちなみに配列ではできてしまう
しかし、配列ではこれができてしまう。
Dog[]dogArray = new Dog[1];
Animal[]animalArray = dogArray;
animalArray[0] = new Cat();
それどころか、以下もできてしまう。
Object[]objectArray = dogArray;
objectArray[0] = 1;
配列が、難しい言葉で言えば非変ではない(共変になっている)理由というのは、ジェネリクスが存在しなかった頃の動作に由来しているようだ。
例えば、Arrays#equals(Object[] a, Object[] a2)というメソッドがあり、これは二つのオブジェクト配列が等しいかを判定するものだが、もし別のオブジェクト型を要素とする配列の代入ができなくなってしまうと、以下のメソッドを新たに揃えなければならない。
- Arrays#equals(Animal[] a, Animal[] a2)
- Arrays#equals(Dog[] a, Dog[] a2)
- Arrays#equals(Cat[] a, Cat[] a2)
- などなどその他無数のメソッド
つまりは、プログラム互換性の観点から配列では現在でも共変(covariant)になっているらしい。
このため、オブジェクト配列を使うのはバグの原因だとする人もいるのだが、自身の個人的な経験上では、これが問題になるのは全く無いと断言してよい。
実際にこんな間違い、つまりあるクラスを要素とする配列を、その上位クラスの配列に代入するなど実際上は無いのだ。
どうしても共変にしたい場合
では、先のArrayList<Dog>()や、新たに作成するArrayList<Cat>()をList<Animal>として扱いたい場合はどうすれば良いのだろうか?
動物リストとして扱えれば、犬リストに対しても、猫リストに対しても、動物に対する共通の処理を行うことができるではないか。
この場合は、「上限付きの境界ワイルドカード型」というのを使う。しかし、こんな言葉はどうでもよい。要するに以下のようなものだ。
List<? extends Animal>animalList;
「Animal以下のクラスである何か」を要素とするリストである。すると、以下のように書ける。
// 犬リストを作り
List<Dog>dogList = new ArrayList<Dog>();
// 「何かしらの動物リスト」に入れる
List<? extends Animal>animalList = dogList;
実用上は上のような記述をすることは無いだろう。
例えば以下のように、「何かしらの動物リストという引数」に代入するだろう。
void mainProc() {
List<Dog>dogList = new ArrayList<Dog>();
processAnimalList(dogList);
}
/** 動物リストに対して何らかの処理をする */
void processAnimalList(List<? extends Animal>animalList) {
}
しかし、最初の「共変にした場合には、あらぬ物がリストに格納されてしまう」という問題はどうなるのだろう?
だから共変が禁止されていたわけなのだが。。。。これをやろうとすると、実はエラーになってしまう。
// 「何かしらの動物リスト」という引数
void processAnimalList(List<? extends Animal>animalList) {
animalList.add(new Cat()); // エラー
animalList.add(new Dog()); // もちろんこれもエラー
animalList.add(new Animal()); // これさえもエラー、なんで?
}
「List<? extends Animal>」は「Animal以下のクラスである何かのリスト」なのだから、犬リストでも、猫リストでもありうる。
したがって、CatやDog、Animalでさえも格納されてはならないわけだ。もしかしたら、Dogリストのさらに下位のSheepDogリストかもしれない。結局、実際には何のリストかさっぱりわからないのである。「動物ではある」ことがわかっているだけだ。
この時に表示されるエラーメッセージは、「The method add(capture#1-of ? extends Animal) in the type List<capture#1-of ? extends Animal> is not applicabble for the arguments (Cat)」などとなる。
しかし、逆にリストからAnimalとして取り出すことはできる。少なくとも要素がAnimalであることは保証されているからだ。
void processAnimalList(List<? extends Animal>animalList) {
Animal animal = animalList.get(0); // OK
}
リストに限らず共変にすると。。。
上述はもちろんListやArrayListに限ったことではない。例えば以下を考えてみる。
/** 動物用のオリ */
class Cage<T extends Animal> {
T animal;
void put(T animal) { // 入れる
this.animal = animal;
}
T get() { // 出す
return animal;
}
}
void mainProc() {
// 犬用オリを作って入れる
Cage<Dog> dogCage = new Cage<Dog>();
dogCage.put(new Dog());
processCage(dogCage);
}
/** 何かしらの動物用オリを処理する */
void processCage(Cage<? extends Animal>animalCage) {
animalCage.put(new Dog()); // もはや犬も入らずエラー
animalCage.put(new Animal()); // これさえもエラー
Animal animal = animalCage.get(); // 動物として出すのはOK
}
共変にしたオブジェクトについては、何も入らなくなる。メソッド引数にその型があると何も指定できなくなるのだ(ただし、nullだけは指定できるようだ)。逆に取得することはできる。
つまり、Putすることはできず、Getのみができるというわけだ。これを「GetとPutの法則」というらしいが、こんな用語はどうでもよい。
ともあれ、DogやCat用のケージであっても、Animalの共変ケージとして扱うことによって、「動物用ケージ」に対する読み込み処理はできるというわけである。
書き込みはできない。ここだけ覚えておけばよい。
反変とは?
さて、次に難しい言葉で反変(contravariance)というのがある。「? super ナンチャラ」と書くのだそうだ。そして、この意味としては、「ナンチャラかその上位クラス」ということである。なぜこんなものが必要かは、例を見た方が早い。
今、犬リストと猫リストがあり、それらを合わせて動物リストに格納したいとする。すると以下のように書ける。ここではまだsuper ナンチャラは使用する必要はない。
void mainProc() {
List<Dog>dogList = new ArrayList<Dog>();
List<Cat>catList = new ArrayList<Cat>();
List<Animal>animalList = new ArrayList<Animal>();
copy(dogList, animalList);
copy(catList, animalList);
}
void copy(List<? extends Animal>src, List<Animal>dst) {
src.stream().forEach(e->dst.add(e));
}
では、上記のanimalListの定義箇所を「List<Object>objectList = new ArrayList<Object>();」としたい場合はどうするか?DogとCatが共通して持つ上位クラスとしては、Animalの上位のObjectもあるので、Objectのリストとして束ねることも可能なはずである。
しかし、このままではエラーになってしまう。
List<Object>objectList = new ArrayList<Object>();
copy(dogList, objectList); // エラー。List<Animal>にList<Object>を引数として渡せない。
copy(catList, objectList); // エラー。List<Animal>にList<Object>を引数として渡せない。
このような場合には、以下のように書く。
void copy(List<? extends Animal>src, List<? super Animal>dst) {
src.stream().forEach(e->dst.add(e));
}
dstリストの要素としては、Animalかその上位クラスなのだから、Animalでもよければ、Objectクラスでもよい。
この場合は共変の場合とはまるでふるまいが逆になり、値が格納できるようになるのである。srcの要素は少なくともAnimalかその下位クラスであり、dstの要素は少なくともAnimalかその上位クラスだからだ。
しかし、ここも共変の場合と逆で、まともな値は取得できない。例えば以下のように書いてみる。
void copy(List<? extends Animal>src, List<? super Animal>dst) {
Animal a = dst.get(0); // エラー。Animalとは限らない。その上位クラスの可能性がある。
Object o = dst.get(0); // OK。常にObjectではあるはず。
}
つまり、Listの場合、その要素として格納可能な最上位のクラスとしてはObjectなので、「? super Animal」の要素はObjectとしてしか取得できないのである。
元の型に制約のある場合
これを専門的な言葉で何というのかわからないのだが、「? super ナンチャラ」を使った場合には、常にObjectでしか取得できないわけではない。例えば、例を以下のように変更してみる。
public static class Animal {}
public static class Dog extends Animal {}
public static class SheepDog extends Dog {}
public static class AkitaDog extends Dog {}
public static class Cat extends Animal {}
class Cage<T extends Animal> {
T animal;
void put(T animal) {
this.animal = animal;
}
T get() {
return animal;
}
}
void mainProc() {
Cage<SheepDog>sheepCage = new Cage<SheepDog>();
Cage<AkitaDog>akitaCage = new Cage<AkitaDog>();
Cage<Dog>dogCage = new Cage<Dog>();
copy(sheepCage, dogCage);
}
void copy(Cage<? extends Dog>src, Cage<? super Dog>dst) {
dst.put(src.get());
Animal a = dst.get(); // Dogかその上位クラスのはずだが、Animalが取得できる。
Dog d = dst.get(); // もちろんこれはエラー
}
見ての通りだ。「Cage<? super Dog>」はDogかその上位クラスのはずなのだが、Cage自体に「T extends Animal」という制約がついているため、最上位クラスはObjectではなく、Animalなのである。だからAnimalとして取得することができる。
Stackoverflowでの質問と答え
最後にStackoverflowでのQ&Aを訳してみる。Difference between <? super T> and <? extends T> in Javaである。
質問
List<? super T>とList<? extends T>の違いは何だろうか?List<? extends T>を使っていたのだけど、しかしlist.add(e)ができないんだ。しかし、List<? super T>の方はできるね。
extendsについての答え
List<? extends Number> foo3という定義では、以下のどれもが正当な代入になる。
List<? extends Number> foo3 = new ArrayList<Number>(); // Number "extends" Number (in this context)
List<? extends Number> foo3 = new ArrayList<Integer>(); // Integer extends Number
List<? extends Number> foo3 = new ArrayList<Double>(); // Double extends Number
読み込み
上記の可能性のある代入において、foo3から読み込めるものは何だろうか?
- Numberを読み込める。なぜなら、foo3に代入されたいずれのリストもNumberあるいはNumberの下位クラスを含むからだ。
- Integerは読み込めない。foo3はList<Double>の可能性があるからだ。
- Doubleは読み込めない。foo3はList<<Integer>の可能性があるからだ。
書き込み
上記の可能性のある代入において、foo3リストに格納なものは何だろうか?
- Integerは加えられない。List<Double>の可能性があるからだ。
- Doubleは加えられない。List<Integer>の可能性があるからだ。
- Numberは加えられない。List<<Integer>の可能性があるからだ。
List<? extends T>には何も加えられないのだ。なぜなら、それが示しているのが、実際にはどのような種類のListなのか保証できないからだ。だから、そのListで何が許されるのかを保証することはできない。唯一「保証」されることは、そこから読み込むことだけで、それがTかTの下位クラスだということだ。
superについての答え
さて、List<? super T>を考えてみよう。List<? super Integer> foo3という定義では、以下のいずれもが正当な代入になる。
List<? super Integer> foo3 = new ArrayList<Integer>(); // Integer is a "superclass" of Integer (in this context)
List<? super Integer> foo3 = new ArrayList<Number>(); // Number is a superclass of Integer
List<? super Integer> foo3 = new ArrayList<Object>(); // Object is a superclass of Integer
読み込み
上記の可能性のある代入において、foo3から読み込めるものは何だろうか?
- Integerは保証できない。なぜなら、foo3はList<Number>あるいはList<Object>かもしれないからだ。
- Numberは保証できない。なぜなら、foo3はList<Object>かもしれないからだ。
- 唯一保証できることは、Objectあるいはその下位クラスのインスタンスであることだ(ただし、どの下位クラスかはわからない)。
書き込み
上記の可能性のある代入において、foo3に追加できるものは何だろうか?
- Integerを追加できる。いずれのリストにおいても可能だからだ。
- Integerのサブクラスを追加できる。いずれのリストにおいてもIntegerが許されるからだ。
- Doubleは追加できない。foo3はArrayList<Integer>かもしれないからだ。
- Numberは追加できない。foo3はArrayList<Integer>かもしれないからだ。
- Objectは追加できない。foo3はArrayList<Integer>かもしれないからだ。