Hatena::ブログ(Diary)

プログラマーの脳みそ このページをアンテナに追加 RSSフィード

2010-11-07

ジェネリクスの代入互換のカラクリ

| 19:01 |  ジェネリクスの代入互換のカラクリを含むブックマーク

Javaジェネリクス再入門 - プログラマーの脳みそでは、「変数の型の宣言」の項で「ジェネリクスの<>の中は一般のJavaの型の代入互換性とは異なる。このことはよく覚えておかなくてはいけない。」と言ったものの、深入りはしなかった。

このあたりについて深入りしてみようじゃないか。

とりあえずサンプルコードはJavaで記述していくが、このあたりはジェネリクス指向の概念の部分だから、あまり言語に依ることはない。便宜的にJavaで書く、としておこう。

まず、型変数の境界について考えるために以下の継承関係のクラスを用意しておく。

public class A {}
public class B extends A {}
public class C extends B {}

public class B2 extends A {}
public class C2 extends B {}

これは図で表すと

A ┬ B ─┬ C
 └ B2  └ C2

という継承関係となる。

さて、ジェネリックなクラスが必要となるのだけど、ここではjava.util.ArrayList<E>を例として用いることにする。クラスの概要としては可変長配列を表現したもので、内部的には配列(Array)でデータ構造を持つ。まあ細かいことはどうでもいいのだが、このクラスにデータを出し入れするためにふたつのメソッドを用いる。

get(int) : E
put(E) : E

getはインデックスを渡すと該当位置のデータを返す。putは該当位置にデータを格納する。このとき、既存のデータがあった場合はそれを返し、なかった場合はnullを返す。が、putの返り値は通常無視して良い。

さあ、準備はできた。

ArrayList<A> に ArrayList<B>を代入した場合の矛盾

まずは前回のおさらい。

ArrayList<B> bList = new ArrayList<B>();
ArrayList<A> aList = bList; // 本来は代入できないができたと仮定する
aList.add(new A()); // ArrayList<A>にはA型を代入できる
B b = bList.get(0); //ArrayList<B>なのでget()の結果はB型のはず

このような状況で問題となるので、ArrayList<A>型の変数 aList に対してArrayList<B>型の変数 bList を代入することはできないとされる。

ArrayList<? extends A> のカラクリ

じゃぁ、Aを継承した型を実型引数にとるArrayListを格納できるような型というのを考えたらどうなるのだろうか。Javaではこのような型をワイルドカードと境界を用いて書く。

ArrayList<? extends A> aExList = new ArrayList<B>();

さて、aExListは実型引数が「Aを継承した何か」なのだった。このとき、当然ながら

aExList.add(new A());

が可能だとすれば、先程のArrayList<A>の場合と同じ問題が発生する。変数 aExList に収まっているのはArrayList<B>型のインスタンスなのだから、これにA型のオブジェクトがadd()できると型の安全が保証できなくなる。

具体的には、Javaの場合は以下のようなコンパイルエラーが発生する。(Eclipse3.6でのエラーメッセージ)

The method add(capture#1-of ? extends A) in the type ArrayList<capture#1-of ? extends A> is not applicable for the arguments (A)

ArrayList<? extends A>型のメソッドadd(capture-of ? extends A)には引数Aを適用することはできない、といったエラーだ。(#1は複数の型変数に対して?を使ったときに識別するためのコード)

ここでadd(capture-of ? extends A)にでてくるキャプチャという表現だが、ArrayList<E>の型変数Eに<? extends A>をバインドした結果のもの、ぐらいのニュアンスだろうか。とにかく、このcapture-of ? extends A型にたいして、A型を代入することはできないとされる。

capture-of ? extends A型に対して代入可能なのはnull値だけだ。Javaの言語仕様上、null値はあらゆる型にキャスト可能とされているが、それはここでも健在なのだ。

ArrayList<? super B> のカラクリ

次に、Bの親、あるいはさらに先祖の型を実型引数にとるArrayListを格納できるような型というのを考えてみよう。おなじくJavaで記述すると

ArrayList<? super B> aSuList = new ArrayList<A>();

さて、aSuListは実型引数が「Bの先祖の何か」なのだった。このとき、

aSuList.add(new B());

は問題ない。今の場合、aSuListの実体は ArrayList<A>型のインスタンスなわけだが、ArrayList<A>型にはB型オブジェクトをadd()できる。あるいはA型のさらに親、Object型を実型引数にとるArrayList<Object>がaSuListに格納されていたとしても、add(new B())は問題とならない。

aSuList.add(new C());
aSuList.add(new C2());

同様の理屈で、Bを継承したC型やC2型をadd()しても、問題とならないことがわかるだろう。add(capture-of ? super B) に対しては B型および、そのサブクラスインスタンスを代入可能なのである。

戻り値

さて、ArrayList<A> に ArrayList<B>を代入できないという部分から、これを許すArrayList<? extends A>型を考え、その場合に引数の型、capture-of ? extends Aにnull値以外が代入不可能となることで辻褄を合わせていることを確認した。また、親の代入を許すArrayList<? super B>型を考え、この場合は引数B型およびそのサブクラスインスタンスを代入しても問題がおこらないことを確認した。

では、戻り値に型変数が使われている場合はどうなのか。

ArrayList<? extends A>型の変数aExList には ArrayList<B>型のインスタンスが格納されているかもしれないし、ArrayList<C>型のインスタンスが格納されているかもしれない。

しかし、get()でこれら「A型を継承した何か」を取得した場合、それはA型の変数に代入可能である。つまり、戻り値のcapture-of ? extends AはA型に安全にキャスト可能だ。

A a = aExList.get(0);

ArrayList<? super B>型の変数aSuListには、ArrayList<A>型やArrayList<Object>型が格納されている可能性がある。するとget()したときに取得できるオブジェクトB型とは限らず、A型かもしれないし、Object型かもしれない。

そのため、戻り値型capture-of ? super Bは、Javaですべての型の頂点に立つObject型以外には安全にキャストできない。

Object object = aSuList.get(0);

まとめ

変数をもつ変数の代入互換性を考える場合、本質的には型変数が同じでないと型安全ではない。そのため通常はArrayList<A>にはArrayList<B>は代入できない。非変(non-variant)

もし、ArrayList<? extends A>というような型を考えた場合(共変(covariant) )、その性質上、引数型に用いられた型変数に対して、null値以外は代入不可能としなければ型安全にならない。

もし、ArrayList<? super A>というような型を考えた場合(反変(contravariant) )、その性質上、戻り値に用いられた型変数に対して、Object型へしかキャストできないとしなければ型安全にならない。

これらを実現するためにcaptureというものを導入する。その性質は、

capturecaptureへの代入captureをキャスト
capture-of ? extends Bnull値のみ可能B型にキャスト可能
capture-of ? super BB型とそのサブクラスが可能Object型にのみキャスト可能

となる。こうして、型安全が保証されている。

参考

Javaジェネリクス再入門 - プログラマーの脳みそ

ジェネリクスの代入を理解する その1

ジェネリクスの代入を理解する その2

Scalaのジェネリックスを学ぶ - じゅんいち☆かとうの技術日誌

variantという単語から派生した色々な単語の整理 - Togetterまとめ

no title

no title