Open30

ジェネリクス: 共変・反変・不変

ほげさんほげさん


変性

T2 extends T1 のとき

  • 不変 → List<T1>List<T2> に関係がない
  • 共変 → List<T2> extends List<T1>
  • 反変 → List<T1> extends List<T2>
ほげさんほげさん
  • 共変 → T を引数に使えない
  • 反変 → T を戻り値に使えない
  • 不変 → T をどちらにも使える
ほげさんほげさん

境界ワイルドカード型と非境界ワイルドカード型で共変と反変を表現できる

Kotlin は out / in で、Scala は + / - で変性を変えられる

ほげさんほげさん


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);
    }
}
ほげさんほげさん


変数 animalDog を抽象化して扱ってるだけで、Animal に変換しているわけではない

だから Dog として使えるし、Cat にはなれない

Animal animal = new Dog();

animal.animalFunction(); // animal

((Dog) animal).dogFunction(); // dog

((Cat) animal).catFunction(); // ClassCastException
ほげさんほげさん


仮に Java の変性が共変だとしても同様

変数 holderHolder<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)TAnimal だからコンパイルは通ってしまうが、実行するとエラーになってしまう

ほげさんほげさん


反変だとすると 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()TDog だからコンパイルは通ってしまうが、実行するとエラーになってしまう

ほげさんほげさん


不変だと問題が起きない

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 は不変なのでこれはできない

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);
}
ほげさんほげさん

getValueObject としてしか取れず、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 のリスト
ほげさんほげさん


https://ufcpp.net/study/csharp/sp4_variance.html

変数は参照だから、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
    }

os1ss1 が代入できてしまうと、os1setObject を上書きできてしまう

public static void main(String[] args) {
    List<String> ss1 = Arrays.asList("foo", "bar");
    List<Object> os1 = ss1;

    os1.set(1, 42);
}
ほげさんほげさん

https://maku77.github.io/kotlin/generics/variant.html

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 なのか