Javaの反変・共変

反変・共変

共変とは

型の順序関係を維持する (≤ で順序づけたとき、特殊から一般の順になる) とき、共変である (covariant) という。 wiki

反変とは

型の順序関係を反転させる (≤ で順序づけたとき、一般から特殊の順になる) とき、反変である (contravariant) という。 wiki

不変とは

上記いずれにも該当しないとき、不変である (invariant) という。 wiki

Javaによる例示

の前に、Javaジェネリクス型以外は共変である。
が、ジェネリクスにおいては不変である。

共変(特殊→一般という変換例)

通常の型

f:id:metatrading:20200407114031p:plain

// 通常の型での共変
Object object = new Object();
String mojiretsu = "あああ";
object = mojiretsu; // 共変により代入可能

配列

// 配列
String[] 特殊 = new String[]{""};
Object[] 一般 = new Object[]{1};

一般 = 特殊; // 共変により代入可能

ジェネリクス

List<Object> 一般 = new ArrayList<>();
List<String> 特殊 = new ArrayList<>();

一般 = 特殊; // コンパイルエラー(ジェネリクスは不変のため)

反変(一般→特殊)

通常の型

Object object = new Object();
String mojiretsu = "あああ";
mojiretsu = object; // コンパイルエラー(反変のため)

配列

String[] 特殊 = new String[]{""};
Object[] 一般 = new Object[]{1};

特殊 = 一般;// コンパイルエラー(反変のため)

ジェネリクス

List<Object> 一般 = new ArrayList<>();
List<String> 特殊 = new ArrayList<>();

特殊 = 一般; // コンパイルエラー(ジェネリクスは不変のため)

境界型パラメータ

前提として、
Animalクラス これを継承したDogクラスやCatクラスがあるものとする。

上限境界ワイルドカード

上限境界は、ジェネリクスに共変をもたらすために用いる。
共変つまり、ジェネリクスのリスト(List<? extends Animal>に共変性(特殊→一般)をもたらす。

List<? extends Animal> animalList = new ArrayList<>();
List<Dog> dogList = new ArrayList<>();
animalList = dogList;

animalListに共変性がもたらされているため、変数としてdogListの代入(特殊→一般)が可能になっている。

下限境界ワイルドカード

下限境界は、ジェネリクスに反変をもたらすために用いる。

List<? super Dog> dogList = new ArrayList<>();
List<Animal> animalList = new ArrayList<>();
dogList = animalList;

dogListに反変性がもたらされているため、変数としてdogListにanimalListの代入(一般→特殊)が可能になっている。

上限境界ワイルドカードの使い所

メソッド引数型

特化した要素を持つListや汎化した要素を持つList受け取る。 そして、汎化したクラスで展開できる。

private void kyouhenAgrs(List<? extends Animal> animalList){
    animalList.forEach(e->{
        // Animal型として展開。
    });
}

これはこういった形でAnimalやその特化した要素を持つListで呼び出せる。

kyouhenAgrs(new ArrayList<Dog>());
kyouhenAgrs(new ArrayList<Cat>());
kyouhenAgrs(new ArrayList<Animal>());

メソッド返却型

特化したListを同じ表現で返却できる。

private List<? extends Animal> kyohenReturnDog(){
    return new ArrayList<Dog>();
}
private List<? extends Animal> kyohenReturnCat(){
    return new ArrayList<Cat>();
}
List<Animal> animalList = new ArrayList<>();
List<? extends Animal> dogList = kyohenReturnDog();
List<? extends Animal> catList = kyohenReturnCat();

// 統一的に扱えるため、集約ができる。
animalList.addAll(dogList);
animalList.addAll(catList);

できないこと

パラメータ化された型を引数に取るメソッド呼び出しは不可能。
? extends Animalに相当する型を引数に取るものは呼び出せない。

List<? extends Animal> animalList = new ArrayList<>();
// すべてパラメータ化された型を引数に取る
animalList.add(new Animal()); // コンパイルエラー
animalList.add(new Dog()); // コンパイルエラー
animalList.add(new Cat()); // コンパイルエラー
animalList.add(new Object()); // Objectですらコンパイルエラー

List#addは

boolean add(E e);

であり、
Listは

public interface List<E> extends Collection<E> {
}

であるため、addで受け取るEとは、パラメータ化された型である。

下限境界ワイルドカードの使い所

Animal ->Dogの継承関係では、 List<? super Dog> は、Dogを下限とするListである。
継承で見るとこんな感じでDogより上の関係をすべて内包可能とする。 つまり、Listには、Dog, Animalそして、すべての祖であるObjectが入りうるが、確定できるのはObjectであることだけである。
ということで、取り出し時は、Object型でしか取り出せないということである。
f:id:metatrading:20200407114103p:plain

メソッド引数型

private void callHanpenArgsDog(){
    // List<Dog>, List<Animal>, List<Object>というDogの親以上を
    // 型パラメータに持つListを引数にできる。
    hanpenArgsDog(new ArrayList<Dog>());
    hanpenArgsDog(new ArrayList<Animal>());
    hanpenArgsDog(new ArrayList<Object>());
}

private void hanpenArgsDog(List<? super Dog> dogList) {
    dogList.forEach(e -> {
        // 取りだしはObjectになる。
    });
}

メソッド返却型

上で述べたように<? super T>は、Objectであることしか確定できないため、
取り出し時はすべてObjectになる。
返却型として指定するメリットは無いように思われる。
※後述する「上限付きクラスと下限付き変数型の組み合わせ」の項ではObject以外に確定することができるので、そちらを活用すれば返却型に定義する意味合いが出てくる。

JDK内の利用事例

java.util.Collections#addAll
    public static <T> boolean addAll(Collection<? super T> c, T... elements) {
        boolean result = false;
        for (T element : elements)
            result |= c.add(element);
        return result;
    }

可変長の実装Tを、Tを汎化して保持するコレクションへ追加する。 非常にわかりやすい。Tは、TおよびTの親であればコレクションに格納できるだろう。

Animal, Dogの例でいうと、こんな感じで呼び出せる

Collections.addAll(new ArrayList<Animal>(),new Dog());
Collections.addAll(new ArrayList<Animal>(),new Cat());
Collections.addAll(new ArrayList<Animal>(),new Animal());
java.util.Collections#copy
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        int srcSize = src.size();
        if (srcSize > dest.size())
            throw new IndexOutOfBoundsException("Source does not fit in dest");

        if (srcSize < COPY_THRESHOLD ||
            (src instanceof RandomAccess && dest instanceof RandomAccess)) {
            for (int i=0; i<srcSize; i++)
                dest.set(i, src.get(i));
        } else {
            ListIterator<? super T> di=dest.listIterator();
            ListIterator<? extends T> si=src.listIterator();
            for (int i=0; i<srcSize; i++) {
                di.next();
                di.set(si.next());
            }
        }
    }

国内で探した限りは、非常によく見るPECSの例。
提供する主体をProducerと呼び、消費する主体をCustomerと呼ぶ。
Producerはextends、Customerはsuperとして、PECS。
srcがProducerでTを上限とするListとなり要素を提供する。
destがCustomerでTを下限とするListとなり、要素を消費する。

Collections.copy(new ArrayList<Animal>(), new ArrayList<Dog>());
Collections.copy(new ArrayList<Animal>(), new ArrayList<Animal>());
java.util.List#sort
    default void sort(Comparator<? super E> c) {
        Object[] a = this.toArray();
        Arrays.sort(a, (Comparator) c);
        ListIterator<E> i = this.listIterator();
        for (Object e : a) {
            i.next();
            i.set((E) e);
        }
    }

これもかなりわかりやすい。

List<Animal> animalList = new ArrayList<>();
Comparator<Animal> animalSorter = Comparator.comparing(Animal::no);
animalList.sort(animalSorter);

List<Dog> dogList = new ArrayList<>();
dogList.sort(animalSorter);

List<Animal>でもList<Dog>でも、Comparator<Animal>sortできるようになっているべきだろう。
sortシグネチャComparator<E>だと、List<Dog>sortanimalSorterではできない。

上限付きクラスと下限付き変数型の組み合わせ

最後に載せているリンク集にある記事内で言及されているものを確認したい。

まず、以下2つのクラス群を定義してみる。

f:id:metatrading:20200407113909p:plain
水クラス群

Water->DrinkingWater->Perieは、そのまま水、飲み水、ペリエと具体化していくクラス。

f:id:metatrading:20200408095424p:plain

WaterCupDrinkingWaterCupPerieCupは、それぞれ水を入れられるカップ、飲み水を入れられるカップペリエを入れられるカップとなる。それぞれ型パラメータで上限境界を設定している。
WaterCupには、Water以下が存在することができ、Waterとして取得可能。
extendsパラメータを与えることで、Producerとしての立ち位置としている。

だがextendsのみだと上限境界の例のとおり、何も設定することはできないクラスとなる。
ここからさらに、下限境界指定した変数宣言を取り入れる。

WaterCup<? super Water>には何でも注げる。

WaterCup<? super Water> waterCupLowerWater = new WaterCup<>();
// TをWaterもしくはWaterの親以上にした。Waterなので多態により、Water以下を受け入れる。
waterCupLowerWater.setWater(new Water());
waterCupLowerWater.setWater(new DrinkingWater());
waterCupLowerWater.setWater(new Perie());
// TはWaterもしくはWaterの親以上なので、Waterは確定できる。
waterCupLowerWater.getWater().waterMethod();

下限境界をDrinkingWaterに変えてみる。
WaterCup<? super DrinkingWater>には、DrinkingWaterおよびPerieが注げる。

WaterCup<? super DrinkingWater> waterCupLowerDrinkingWater = new WaterCup<>();
// 下限をDrinkingWaterで与えられているため、値の設定が可能。
// 値自体は、T、もしくはTの子以下を設定可能。つまり、DrikingWater、その子のPerieは設定可能。
//        waterCupLowerDrinkingWater.setWater(new Water()); // compile error
waterCupLowerDrinkingWater.setWater(new DrinkingWater());
waterCupLowerDrinkingWater.setWater(new Perie());
// extends はWaterCupに指定されたwaterとなるため、waterであることが確定できる。
waterCupLowerDrinkingWater.getWater().waterMethod();

DrinkingWaterCup<? super Perie>の例

DrinkingWaterCup<? super Perie> drinkingWaterCup = new DrinkingWaterCup<>();
drinkingWaterCup.setWater(new Water()); // コンパイルエラー
drinkingWaterCup.setWater(new DrinkingWater()); // コンパイルエラー
drinkingWaterCup.setWater(new Perie());
drinkingWaterCup.getWater().drinkingwaterMethod();

PerieCup<? super Perie>の例

PerieCup<? super Perie> perieCup = new PerieCup<>();
perieCup.setWater(new Water()); // コンパイルエラー
perieCup.setWater(new DrinkingWater()); // コンパイルエラー
perieCup.setWater(new Perie());
perieCup.getWater().perieMethod();

こういった組み合わせをまとめるとこうなる。
Xはコンパイルエラー。 f:id:metatrading:20200407164718p:plain

f:id:metatrading:20200408095958p:plain

f:id:metatrading:20200408100019p:plain

extendsの指定がProducerの定義となり、getの返却型(上限境界)を決める。
変数に定義したsuperの指定がConsumerの定義となり、setの受け入れ可能な型(下限境界)を決める。

リンク集