読者です 読者をやめる 読者になる 読者になる

C Sharpens you up

http://qiita.com/yuba に移しつつあります

ガチで5分で理解できるワイルドカード総称型("? extends C"/"? super C")

Java

もしくは、「Iterable<Number>にIterable<Double>を代入したいっ!」。もしくは「ガチで5分で理解できる共変・反変」、どれでもいいです。同じことです。

どういうことかというと

NumberDoubleに親子関係があったときにIterable<Number>Iterable<Double>にも親子関係が発生することを共変関係と呼び(逆の親子関係が発生するなら反変関係)、Javaで共変関係を生かしたままジェネリッククラスを扱う道具がワイルドカード総称型です。

はい、わかったようなわからないような説明ですね。

実務でありそうな例を見ながら書いていきます

ファクトリとプロダクトのインターフェース(Factory, Product)があったとして、それに実装クラス(FactoryImpl, ProductImpl)を書くとします。ファクトリの機能として、作成済みプロダクトを一気に取得するgetAllInstances()メソッドを作ろうとか、ありそうな要求ですよね。

そのgetAllInstancesの戻り値は当然List<Product>Iterable<Product>あたりになりますが、FactoryImplでは内部的にArrayList<ProductImpl>で持っていたりします。

するとどうでしょう。ArrayList<ProductImpl>型のフィールド_productsはそのまま

public Iterable<Product> getAllInstances() {
  return _products;
}

と返したりできません。コンパイルエラーでJenkins執事長から怒りのエラーメールです。Iterable<ProductImpl>Iterable<Product>の継承インターフェースではないからです。

Iterable<ProductImpl>Iterable<Product>の機能を完全にカバーしている(ProductImplを吐き出すということは、Productを吐き出していると言えます)ので、これは継承インターフェースだと言い切っちゃっていいって人間はわかっているんですけどね。コンパイラはわかってくれない。さあどうする。

C#の場合

C#ですとそういう問題はありません。

public IEnumerable<Product> GetAllInstances() {
  return _products;
}

でコンパイルが通ります*1C#のIEnumerable、これがIEnumerable<Product>からIEnumerable<ProductImpl>の継承関係を許している理由は、IEnumerableの宣言を見るとわかります。

public interface IEnumerable<out T>

このoutがポイント。T型は戻り値としてしか使わないと約束するので共変パラメータだ(IEnumerable<Tの派生型>がIEnumerable<T>の派生型だ)と認めてください、とコンパイラにお願いしているのです。こう約束しているからTを引数に持つメソッドは書けません。書いたらコンパイルエラー。それによって共変性が保証されます。

Javaの場合

JavaC#と全く逆のやり方で共変・反変を宣言します。C#ではクラス宣言でそれを宣言していました。Javaではクラスを使うコードの方が「これ共変として使うからね!」と宣言します。ええ、勝手に。呼び出し側で勝手に宣言できる代わり、共変を宣言するとその型を引数に取るメソッドが呼び出し不能になる(呼び出そうとするとコンパイルエラーになる)という仕組み。

破れかぶれですが整合性は保証できています。

共変の宣言の仕方はこう。

Iterable<? extends Product> products;

変な書き方をしました。この宣言で変数productsは何型になっているか?

こう考えましょう。
共変宣言のついた特殊なIterable<Product>。Productを引数に取るメソッドは呼び出し禁止だがIterable<ProductImpl>のような型のオブジェクトも代入できる。

もっとも、Iterableには最初からProductを引数に取るメソッドなんかないので不便はありません。
つまり最初に書きたかったgetAllInstancesメソッドはこう書けるわけです。

public Iterable<? extends Product> getAllInstances() {
  return _products;
}

一件落着。

反変については

全部あべこべなのでいちいち書きません。類推よろしくです。
outの反対はinだし、引数の反対は戻り値だし、extendsの反対はsuperです。

比較して

Java? extends Cを破れかぶれとか激disしておいてなんですが、これはこれでいいところがあるんですね。本来は共変にも反変にもならないはずの(「不変の」)クラスでも、使い方が限定される場合にはこっちから共変・反変を設定できるからです。

たとえばC#のIDictionaryインターフェース(JavaでいうMapインターフェース)。これは不変です。キーの型はほとんど引数としてしか使わないので反変でも良さそうですが、キーをすべて取得するメソッドがありますからね。反変にできません。

とことがJavaならどうだ、「キーの全取得? しないそんなの」となった時点で反変宣言のついたMap<? super Integer, String>なんて型の変数を使っていけるわけです。

ここまで読んでくれた皆様としてはどちらがお好みですか?

ちなみに、クラス宣言でも変数宣言でもどちらでも使えたらいいのにと思った方、Scalaやりましょう。

*1:C#の流儀ではProductインターフェース、ProductImplクラスと命名せずにIProductインターフェース、Productクラスと命名するものですが、ここは最初の例にそのまま従います。ついでに言えば、C#の流儀ならこれはきっとメソッドでなくプロパティにしますね。