Java Generics Hell - 共変ワイルドカード

Java Generics Hell アドベントカレンダー 13日目。

読者の推奨スキルとしてはOCJP Silverぐらいを想定している。

変性

パラメタライズドタイプについては以前に書いたが、ワイルドカードについては後回しということにしていた。

<>の内部については非変性であるということは何度か述べてきたが、特定の制限下で共変性、あるいは反変性をもたせることは出来るだろうか?

共変性

パラメタライズドタイプが共変である場合、何が問題になるのだったか再確認しよう。以下ではclass Aを親とし、class Bが子供という関係とする。パラメタライズドタイプを共変とするとき? extends を用いて以下のように記述する。

List<B> listB = new ArrayList<B>();
List<? exnteds A> listExA = listB; // 代入可能

ここでList<B>型の機能性について確認しておこう。

  • listB は B型を格納することが出来る
  • listB から B型を取り出すことができる

比較用に List<A>型のlistAがあったとして、この機能性についても確認しよう。

  • listA は A型を格納することが出来る
  • listA から A型を取り出すことができる

となるのであった。問題となるのはA型を格納の部分で、リスコフの置換原則に従うならList<A>型を継承するならList<A>型の機能すべてを子は満足させないといけないのであった。List<B>はList<A>の機能を満足させることができない。B型しか格納することが出来ず、A型を格納できないのだから。よってList<B>型をList<A>型の子として暗黙に代入可能としてしまうと問題がおきるのであった。

ここで、制約、つまり、格納する機能をそもそも使わないという前提をおいたら整合性が取れるのではないか?つまり

  • listExA は格納することが出来ない
  • listExA から B型を取り出すことができる

という条件であれば、listBをlistExAに代入できても矛盾は生じない。

格納することが出来ないとはどういうことか?これは引数に型変数が用いられているものを用いないということだ。例えばjava.util.Listであれば

public boolean add(E e) { ... }

は引数に型変数Eが用いられているので listExA.add( ... ) を使わせなければいい。

矛盾するものを使わせなければ矛盾しないということになる。

キャプチャ

さて、おおまかな方針は述べたものの、実は多分に方便が混ざっていて正確ではない。概要を踏まえて詳細に入ろう。

List<? extends A>型では型変数Eは? extends Aとなる。Aというわけではない。この? extends Aはキャプチャ(capture)と呼ばれる。このキャプチャという用語、コンパイルエラーで見かけることがある。

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

キャプチャ ? extends Aの性質は以下のようなものになる。

  • A型 を ? extends A に変換できない
  • ? extends A はA型に暗黙に安全に変換できる

このため、List<? extends A>型のadd(E)はEにキャプチャ? extends Aがバインドされ、addの引数に? extends A型に変換可能なものしか受け付けない。しかし、A型は? extends Aに変換できないのでaddの引数にA型は渡せない、というわけだ。ただし、add()に何も渡せないかというと例外がある。

? extends A にキャスト可能なのはnullリテラルだ。前回 nullは全ての型にキャスト可能という話をしたが、キャプチャに対してもキャスト可能である。なのでadd(null)だけは可能となっている。

ここで、? extends A はA型に暗黙に安全に変換できるので、getについては

A a = listExA.get(0);

といった形で使用することができる。

共変ワイルドカードの意義

List<? extends A>型にはList<A>型やList<B>型を代入することが出来る。これをメソッドの引数に用いるとどうなるか。

public void hoge(List<? extends A> list) { ... }

このようにすると、hogeメソッドにはList<A>を渡すこともList<B>を渡すこともできるようになる。
hogeメソッド内部では、get()を用いることはできても、add()などで引数のListに変更を加えることはしない。
メソッド内部では、共変ワイルドカードで渡されたオブジェクトから値を取ることが出来る。

これにより、引数の型を柔軟にしつつ、型の安全性を保つことができる。これが ? extends Aの重要な意義である。