ジェネリクス: 共変・反変・不変
✅
変性
T2 extends T1
のとき
- 不変 →
List<T1>
とList<T2>
に関係がない - 共変 →
List<T2> extends List<T1>
- 反変 →
List<T1> extends List<T2>
- 共変 →
T
を引数に使えない - 反変 →
T
を戻り値に使えない - 不変 →
T
をどちらにも使える
境界ワイルドカード型と非境界ワイルドカード型で共変と反変を表現できる
Kotlin は out / in
で、Scala は + / -
で変性を変えられる
Producer-Extends Consumer-Super
✅
Java は不変なので Holder<Animal>
と Holder<Dog>
に継承関係はない
代入できない
Holder<Animal> animals = new Holder<Dog>(new Dog());
🙅♂️
🙅♂️
変数 | Class | Generic Type |
---|---|---|
Holder<Animal> |
Holder |
Holder<Animal> |
Holder<? extends Animal> |
Holder |
Holder<? extends Animal> |
checker
package langbox.generic;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
public class Main {
private static Holder<Animal> animals;
private static Holder<? extends Animal> animals2;
public static void main(String[] args) throws NoSuchFieldException {
animals = new Holder<>(new Dog());
animals.setValue(new Cat());
check(animals, "animals");
animals2 = new Holder<>(new Cat());
check(animals2, "animals2");
}
private static void check(Object x, String name) throws NoSuchFieldException {
System.out.println(x);
System.out.println(x.getClass());
Field field = Main.class.getDeclaredField(name);
Type type = field.getGenericType();
System.out.println(type);
}
}
✅
変数 animal
は Dog
を抽象化して扱ってるだけで、Animal
に変換しているわけではない
だから Dog
として使えるし、Cat
にはなれない
Animal animal = new Dog();
animal.animalFunction(); // animal
((Dog) animal).dogFunction(); // dog
((Cat) animal).catFunction(); // ClassCastException
✅
仮に Java の変性が共変だとしても同様
変数 holder
は Holder<Dog>
を抽象化して扱ってるだけで、Holder<Animal>
に変換しているわけではない
だから Holder<Dog>
として使えるし、Holder<Cat>
にはなれない
Holder<Animal> holder = new Holder<Dog>(new Dog()); // もしこれができるなら...
holder.getValue().animalFunction(); // animal
((Holder<Dog>) holder).getValue().dogFunction(); // dog
((Holder<Cat>) holder).getValue().catFunction(); // ClassCastException
✅
共変だとすると setValue
が狂う
Holder<Animal> holder = new Holder<Dog>(new Dog()); // もしこれができるなら...
Animal animal = holder.getValue(); // OK: 実体は Holder<Dog> なので Animal として受け取れる
holder.setValue(new Cat()); // NG: 実体は Holder<Dog> なので Cat は入らない
setValue(T value)
で T
は Animal
だからコンパイルは通ってしまうが、実行するとエラーになってしまう
✅
反変だとすると getValue
が狂う
Holder<Dog> holder = new Holder<Animal>(new Animal()); // もしこれができるなら...
holder.setValue(new Cat()); // OK: 実体は Holder<Animal> なので Cat が入る
Dog dog = holder.getValue(); // NG: 実体は Holder<Animal> なので Dog が取れる保証がない
T getValue()
で T
は Dog
だからコンパイルは通ってしまうが、実行するとエラーになってしまう
✅
不変だと問題が起きない
Holder<Animal> holder = new Holder<Animal>(new Animal());
holder.setValue(new Cat()); // OK: 実体は Holder<Animal> なので Cat が入る
Animal animal = holder.getValue(); // // OK: 実体は Holder<Dog> なので Animal として受け取れる
🙅♂️
余談
Java の配列は共変
ここまで見たように、Java は不変なのでこれはできない
Holder<Animal> holder;
holder = new Holder<Dog>(new Dog());
holder = new Holder<Cat>(new Cat());
境界ワイルドカード型 ( ?
) を使うと、Holder<?>
は Holder
の親クラスになる
Holder<?> holder;
holder = new Holder<Dog>(new Dog());
holder = new Holder<Cat>(new Cat());
なんでもいい場合
public static void main(String[] args) {
print2(new Holder<Dog>(new Dog()), new Holder<Cat>(new Cat()));
}
private static void print2(Holder<?> holder1, Holder<?> holder2) {
System.out.println(holder1);
System.out.println(holder2);
}
そうでない場合
public static void main(String[] args) {
print2(new Holder<Dog>(new Dog()), new Holder<Dog>(new Dog()));
}
private static <T> void print2(Holder<T> holder1, Holder<T> holder2) {
System.out.println(holder1);
System.out.println(holder2);
}
getValue
は Object
としてしか取れず、setValue
は実質できない
Holder<?> holder = new Holder<Dog>(new Dog());
Object animal = holder.getValue(); // 実体は Holder<Dog> だが Holder<Cat> かもしれない、すべてのスーパークラスとしてしか安心して受け取れない
holder.setValue(null); // 実体は Holder<Dog> だが Holder<Cat> かもしれない、すべてのサブクラスしか安心して渡せない
出し入れできないので入れ物としてはあまり役に立たない
出しも入れもしないメソッドを作るときくらいしか使わない
標準出力するとか長さを数えるとか
境界ワイルドカード型は非境界ワイルドカード型の範囲を制限できる
Holder<? extends Animal>
は Animal
のサブタイプで作られた Holder
のスーパークラスになる
Holder<? super Animal>
は Animal
のスーパータイプで作られた Holder
のスーパークラスになる
?
と <? extends Animal>
を比べると、?
の「何が取り出せるかわからないから一番スーパーな Object
で受けるしかない」が「最高でも Animal
までしか出てこないから Animal
で受け取れる」に限定できる
Holder<? extends Number> holder = new Holder<Integer>(Integer.valueOf(1));
Number number = holder.getValue(); // OK: 最上位でも Number どまり
holder.setValue(Double.valueOf(1.5)); // NG: ここは変わらず、実体は Holder<Integer> なので Double は入らない
?
と <? super Animal>
を比べると、?
の「何が入るかわからないから一番サブな null
を入れるしかない」が「最低でも Animal
は入る」に限定できる
Holder<? super Number> holder = new Holder<Object>(new Object());
Object object = holder.getValue(); // 上限はないので ? と同じ
Double d = Double.valueOf(1.5);
holder.setValue(d); // OK: 実体が Holder<Object> で最低でも Number は入る
不変
- 使う
- そのまま渡す
public static void main(String[] args) {
Holder<Number> h = new Holder<Number>(Integer.valueOf(1));
System.out.println(h); // Holder(value=1)
Number n1 = h.getValue();
System.out.println(n1); // 1
h.setValue(Double.valueOf(4.2));
System.out.println(h); // Holder(value=4.2)
Number n2 = h.getValue();
System.out.println(n2); // 4.2
f1(h);
}
private static void f1(Holder<Number> h) {
System.out.println(h.getValue());
h.setValue(Long.valueOf(100));
}
共変
public static void main(String[] args) {
Holder<Integer> h = new Holder<Integer>(Integer.valueOf(1));
Integer n1 = h.getValue();
h.setValue(Integer.valueOf(4));
// f1(h); // Holder<Number> と Holder<Integer> に継承関係がないので渡せない
f2(h); // Holder<? extends Number> <|-- Holder<Integer> なので渡せる
}
private static void f1(Holder<Number> h) {
System.out.println(h.getValue());
h.setValue(Long.valueOf(100));
}
private static void f2(Holder<? extends Number> h) {
Number n = h.getValue(); // Number 以下の何かはとれる
System.out.println(n);
// h.setValue(Integer.valueOf(1)); // こっち目線だと実体がわからないので何も入らない
}
反変
public static void main(String[] args) {
Holder<Number> h = new Holder<Number>(Integer.valueOf(1));
// f1(h); // Holder<Number> と Holder<Integer> に継承関係がないので渡せない
// f2(h); // Holder<? extends Number> <|-- Holder<Integer> なので渡せる
f3(h); // Holder<? extends Number> <|-- Holder<Integer> なので渡せる
System.out.println(h);
}
private static void f1(Holder<Number> h) {
System.out.println(h.getValue());
h.setValue(Long.valueOf(100));
}
private static void f2(Holder<? extends Number> h) {
Number n = h.getValue(); // Number 以下の何かはとれる
System.out.println(n);
// h.setValue(Integer.valueOf(1)); // こっち目線だと実体がわからないので何も入らない
}
private static void f3(Holder<? super Integer> h) {
Object n = h.getValue(); // Integer 以上のなにかなので Object でしかとれない
h.setValue(Integer.valueOf(2)); // Integer 以上の何かと保証されているので Integer が入る
}
🙅♂️
共変のクラスを作ることもできる
public class CoHolder<T extends Animal> {
private T value;
public CoHolder(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
CoHolder<Dog> h = new CoHolder<Dog>(new Dog());
ただし不変のルールが消えるわけではないので CoHolder<Animal>
と CoHolder<Dog>
に継承関係はない
CoHolder<Animal> h = new CoHolder<Dog>(new Dog()); // エラー
Animal
以外で CoHolder<>
が存在できないだけで、get / set や変数の受け渡しについては今までと同じ
CoHolder<Number> h = new CoHolder<Number>(Integer.valueOf(1));
ただし、super
と合わせると上限と下限を指定できる
public static void main(String[] args) {
CoHolder<Animal> h = new CoHolder<Animal>(new Dog());
f4(h);
}
private static void f4(CoHolder<? super Dog> h) {
h.setValue(new Dog()); // いつもの反変
Animal animal = h.getValue();
System.out.println(animal); // 上限の境界はないように見えるが、CoHolder からは Animal しか出てこない
}
List<? extends Animal>
- x
Animal
以下の何かが入るリスト - o
Animal
以下の何かのリスト →Dog
のリスト
✅
変数は参照だから、ss2
を通して ss1
を更新することになる
public static void main(String[] args) {
List<String> ss1 = Arrays.asList("foo", "bar");
List<String> ss2 = ss1;
ss2.set(1, "xxx");
System.out.println(Arrays.toString(ss1.toArray())); // foo, xxx
}
os1
に ss1
が代入できてしまうと、os1
の set
で Object
を上書きできてしまう
public static void main(String[] args) {
List<String> ss1 = Arrays.asList("foo", "bar");
List<Object> os1 = ss1;
os1.set(1, 42);
}
水と容器を使った extends + super の例
val anyComp = Comparator<Any> { a, b ->
a.hashCode() - b.hashCode()
}
val intList = mutableListOf(3, 1, 5)
intList.sortWith(anyComp)
Comparator<Any>
の実装の中ではAny
インタフェースしか参照しないため、その実装をInt
オブジェクト同士の比較に使用しても何ら問題はないからです。
反変が引数に使えるから in
で、共変が戻り値に使えるから out
なのか