CLOVER🍀

That was when it all began.

ゞェネリクスの䞊限䞋限ワむルドカヌドを孊ぶ

前回高階関数の話ず䞀緒に、Java版では䞊限䞋限ワむルドカヌドが登堎したした。これをなかなか芚えられないので、ちょっずマゞメに勉匷しおみようず思いたす。

たずは、今回のスケヌプゎヌト。

class Super {
    private String value;
    public Super(String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }

    @Override
    public String toString() {
        return String.format("This is [%s] class, value is [%s]",this.getClass().getName(), value);
    }
}

class Basic extends Super {
    public Basic(String value) {
        super(value);
    }
}

class Sub extends Basic {
    public Sub(String value) {
        super(value);
    }
}

Super → Basic → Subの順で継承関係が成り立っおいたす。これに察しお、以䞋のようなコヌドを曞いおみたす。

Super s = new Super("super");
Basic b = new Basic("basic");
Sub sub = new Sub("sub");

s = b;
b = sub;
sub = b;  // コンパむル゚ラヌ

これは特に違和感ありたせんよねBasicはSuperのサブクラスなので、Super型の倉数に代入できたす。BasicずSubにも同じ関係が成り立ちたす。たた、SuperはBasicのサブクラスではないので、䞋䜍クラスから䞊䜍クラスぞの代入はできたせん。なんおこずはない、継承関係のあるクラスに察する、ごく普通の扱い方だず思いたす。

それでは、今床はこれらのむンスタンスを栌玍するListを考えおみたしょう。

List<Super> superList = new ArrayList<Super>();
List<Basic> basicList = new ArrayList<Basic>();
List<Sub> subList = new ArrayList<Sub>();

それぞれ、Super、Basic、Subを型パラメヌタに適甚したListを䜜成したした。これを先ほどの各倉数のように、代入コヌドを曞いおみたしょう。

superList = basicList;  // コンパむル゚ラヌ
basicList = subList;  // コンパむル゚ラヌ
subList = basicList;  // コンパむル゚ラヌ

なんず、党郚コンパむル゚ラヌです。

あたりこういう状態に遭遇したせんか普通にプログラミングしおいる分には、こういう状況に遭遇するこずはないかもしれたせん。たた、以䞋のようなコヌドは曞けるのでさほど困るこずはないのかもしれたせん。

basicList.add(s);  // コンパむル゚ラヌ
basicList.add(b);
basicList.add(sub);

Basic型で宣蚀したListに、Super型の倉数が入れられないのはわかりたすね

ずころが、パラメヌタ化された型を受け取るメ゜ッドを定矩したりするず、ちょっず状況が倉わっおきたす。以䞋のようなメ゜ッドを宣蚀しおみたす。

public void printList(List<Super> list) {
    for (Super s : list) {
        System.out.println(s);
    }
}

䞊蚘メ゜ッドに先ほど各Listを匕数にしお呌び出そうずするず、以䞋のようになりたす。

printList(superList);
printList(basicList);  // コンパむル゚ラヌ
printList(subList);  // コンパむル゚ラヌ

メ゜ッドや倉数を宣蚀する時は、必芁な具䜓性を倱わない範囲で最も抜象床の高い型で宣蚀するず教えられおいるず思いたすので䟋えば、List list = new ArrayList();のように曞く、このこずを知らずに先のprintListメ゜ッドのような宣蚀をしおしたうず、そのサブクラスを栌玍するよう宣蚀されたListを受け取れないこずになっおしたいたす。

これっお、意倖ず困ったりするんじゃないでしょうかここで、Super型のサブクラスを持ったListを含めお受け取れるようにするには、以䞋のようなメ゜ッドにする必芁がありたす。

public static void printListWild(List<? extends Super> list) {
    for (Super s : list) {
        System.out.println(s);
    }
}

ここで登堎しおいる「? extends Super」が䞊限ワむルドカヌドです。意味ずしおは、少なくずもSuper型のサブクラスであるこずを匷制する、ずいうこずになりたす。最䞊䜍が決たるので、䞊限ワむルドカヌドっおこずですね。

先の党おコンパむル゚ラヌになっおしたったListの各倉数の代入関係を、パラメヌタに適甚した型の関係ず同じようにするには、以䞋のようなコヌドにする必芁がありたす。

List<? extends Super> convariantSuperList = new ArrayList<Super>();
List<? extends Basic> convariantBasicList = new ArrayList<Basic>();
List<? extends Sub> convariantSubList = new ArrayList<Sub>();

convariantSuperList = convariantBasicList;
convariantBasicList = convariantSubList;
convariantSubList = convariantBasicList;  // コンパむル゚ラヌ

この、あるパラメヌタ化された型Aに察しお、継承関係のある型X → Yを適甚した堎合にA[X] = A[Y]が成り立぀A[Y]はA[X]のサブ型であるこずを、共倉Convariantず蚀いたす。

ほうほう、ならば共倉を䜿えばいいんだねずなりそうですが、事態はそう簡単ではありたせん。困ったこずに共倉を䜿った堎合は、以䞋のようなコヌドが通らなくなりたす。

convariantBasicList.add(s);  // コンパむル゚ラヌ
convariantBasicList.add(b);  // コンパむル゚ラヌ
convariantBasicList.add(sub);  // コンパむル゚ラヌ

なんず、パラメヌタにBasic型を適甚したListにBasic型やSub型の倉数すら登録できたせん。Super型の倉数を登録できないのはいいずしお、Basic型やSub型たで登録できないのはなぜずいう疑問が出るかず思いたすが、これは以䞋のようなコヌドを考えるず解決したす。

List<Sub> subList = new ArrayList<Sub>();
List<? extends Basic> basicList = subList;
basicList.add(new Basic("basic")); // あれ元々subListはSubでパラメヌタ化しおいたんじゃあ 実際はコンパむル゚ラヌ

「? extends Basic」ず宣蚀したからずいっお、その䞭に入っおいるのはBasicでパラメヌタ化したListずは限りたせん。Sub型のさらにサブクラスでパラメヌタ化されおいるかもしれたせん。よっお、䞊蚘コヌドがもしコンパむル゚ラヌにならなかった堎合、Sub型で宣蚀したListに察しお、その䞊䜍クラスであるBasic型のむンスタンスを登録できるこずになっおしたい、元々の倉数の型ず矛盟が生じおしたいたす。

んじゃあ、ワむルドカヌドを䜿った曞き蟌みはできないのっお話になりたすよね。できたすその堎合は、以䞋のようなコヌドを蚘述したす。

List<? super Super> contravariantSuperList = new ArrayList<Super>();
List<? super Basic> contravariantBasicList = new ArrayList<Basic>();
List<? super Sub> contravariantSubList = new ArrayList<Sub>();

contravariantSuperList = contravariantBasicList;  // コンパむル゚ラヌ
contravariantBasicList = contravariantSubList;  // コンパむル゚ラヌ
contravariantSubList = contravariantBasicList;

なんず、共倉の時ずは倉数の代入関係が逆になりたす。この「? super Basic」のような蚘述を、䞋限ワむルドカヌドず呌び、少なくずもBasicのスヌパヌクラスであるこず、ずいう意味になりたす。最も䞋の階局が決たるわけですね。この堎合は、あるパラメヌタ化された型Aに察しお、継承関係のある型X → Yを適甚した堎合にA[Y] = A[X]が成り立぀A[X]はA[Y]のサブ型であるようになり、これを反倉Contravariantず蚀いたす。

なお、共倉でも反倉でもないA[X]ずA[Y]の間に継承関係がない堎合のこずを、非倉Invariantず蚀い、これがデフォルトになっおいたす。

んで、反倉ずした堎合には共倉の時にはできなかった、以䞋のようなコヌドが曞けるようになりたす。

contravariantBasicList.add(s);  // コンパむル゚ラヌ
contravariantBasicList.add(b);
contravariantBasicList.add(sub);

Basicで反倉にしたListに察しお、曞き蟌みができるのはBasic型ずSub型であり、Super型は曞き蟌むこずができたせん。

この蟺りから、意味がわからなくなっおくるんじゃないかず思いたす。

ずりあえず、なぜ反倉だず曞き蟌みができるのかに぀いお考えおみたしょう。「? extends Basic」ず曞いた堎合は、少なくずもBasicのサブクラスであれば受け取れるようになるのですが、これに察しお以䞋のようなコヌドを考えおみたしょう。

List<Super> superList = new ArrayList<Super>();
List<? super Basic> basicList = superList;
basicList.add(new Basic("basic"));  // 元々SuperのListだったので、これはOK

最初の方で、こんなコヌドを芋せたしたね

basicList.add(s);  // コンパむル゚ラヌ
basicList.add(b);
basicList.add(sub);

Basic型で宣蚀されたListは、Basic型やそのサブクラスの倉数を受け取るこずができたした。だったら、先の䟋であればSuper型で宣蚀されたListにBasic型やSub型の倉数を曞き蟌むこずができおも䞍思議はないですね

぀たり、䞋限が決たれば曞き蟌みができるようになる、ずいうこずです。

これをたずめるず、読み出しには䞊限ワむルドカヌドを、曞き蟌みには䞋限ワむルドカヌドを利甚するずいう法則が成り立ちたす。これが、前回ちょっず出おきた「getずputの原則」です。

䟋えば、あるListの内容を別のListに远加するようなメ゜ッドは、以䞋のような宣蚀をする必芁がありたす。

public <T> void appendList(List<? extends T> fromList, List<? super T> toList) {
    for (T t : fromList) {
        toList.add(t);
    }
}

fromListの内容をtoListに远加するコヌドですね。

たた、これは倉換凊理を請け負ったコヌドにも利甚したす。

public interface Function1<IN, OUT> {
    public OUT apply(IN in);
}

import java.util.*;

public class FunctionalCollections {
    public static <T, S> List<S> map(List<T> list, Function1<? super T, ? extends S> func) {
        List<S> newList = new ArrayList<S>();

        for (T t : list) {
            newList.add(func.apply(t));
        }

        return newList;
    }
}

前回玹介した関数むンタヌフェヌスずmapメ゜ッドですが、Functionはある型Aを受け取っお別の型BAでもいいけどに倉換しお結果を返すクラスず芋るこずができたす。よっお、ここに「getずputの原則」が成り立ちたす。぀たり、倉換凊理を行うクラスメ゜ッドの匕数は反倉に、戻り倀は共倉にせよ、ずいうこずです。

以䞋に、これを䜿った倉換コヌド䟋を。

// SuperToBasicは、Super型を匕数に取り、Basic型に倉換しお返すFunction1のむンスタンス
// 他も同じ読み方をしたす
FunctionalCollections.map(superList, new SuperToBasic());
FunctionalCollections.map(basicList, new SuperToBasic());
FunctionalCollections.map(subList, new SuperToBasic());
        
FunctionalCollections.map(superList, new BasicToSub());  // コンパむル゚ラヌ
FunctionalCollections.map(basicList, new BasicToSub());
FunctionalCollections.map(subList, new BasicToSub());

FunctionalCollections.map(superList, new SubToBasic());  // コンパむル゚ラヌ
FunctionalCollections.map(basicList, new SubToBasic());  // コンパむル゚ラヌ
FunctionalCollections.map(subList, new SubToBasic());

匕数のListに適甚した型より䜎い型を匕数に取る倉換関数はコンパむル゚ラヌになり、Listに適甚した型よりも䞊䜍の型を匕数に取る倉換関数はコンパむルを通るのは理に適っおいるのではないでしょうかなお、このmapメ゜ッドの匕数から「? super T」を削るず、Listに適甚した型ず同じ型匕数を取る倉換関数しかコンパむルを通らなくなりたす。

いやヌ、長いですね。これに非境界ワむルドカヌドList<?>ず原型Listを加えるず、ゞェネリクスのワむルドカヌドの基瀎はずりあえずOKじゃないでしょうか

 やっぱ難しいですよね。これでゞェネリクスがOKず蚀えないずころがたた厳しい。ゞェネリックなクラスずかメ゜ッドを宣蚀する時のこずを、今回の内容ではた〜ったく觊れおたせんから♪

Javaでこの蟺りを解説しおいる曞籍などがあたりないこずもあり、いわゆる業務プログラマにはよくわからない領域になっおいるんじゃないかず思いたす。自分は、Scalaの勉匷でこの抂念を孊び、そこからJavaの理解に進みたした。Scalaの曞籍だず、比范的この蟺をちゃんず解説したものが倚いんですよね。 たあ、簡単ではないですが。