Java のジェネリックスにあるワイルドカードについて解説するシリーズの後半。
非境界ワイルドカード型と原型
前回、ワイルドカードの中でも特に「非境界ワイルドカード型」について説明しました。 非境界ワイルドカード型は「ジェネリックスを利用したいけど、型パラメータに何が含まれるか分からない」という場合に利用します。
一方、ジェネリックスには、そもそも型パラメータを与えない「原型」と呼ばれるものがあります(参考:項目23)。
この「原型」も非境界ワイルドカードと同じようなケースで利用することができます。 ただし、項目23で述べたように、一般的には原型の利用は推奨されません。 原型よりも非境界型ワイルドカードを利用することが推奨されています。
原型ではコンパイラによる型安全性は提供されないことが理由です。 逆に、非境界ワイルドカードではコンパイラによる型安全性が提供されます。
上で「コンパイラによる」と書いたように、非境界ワイルドカード型と原型の差は静的なものです。 実行時、ジェネリックスの型情報はすべて削除されます。 そのため実行時には原型と非境界ワイルドカード型に差はないのです。
原型と型安全
では、原型と非境界ジェネリックスの型安全性の差を見てみます。 前の記事で、非境界ジェネリックスには以下の制限があると述べました。
前回利用した Holder ジェネリックスを使って、コード上でこの性質を確認してみます。
Holder<?> wildcard = new Holder<String>("String"); Object object = wildcard.getValue(); // => 問題なし String string = wildcard.getValue(); // => コンパイルエラー // メソッドの戻り値に使われる T は Object 型になるため、String には代入できない wildcard.setValue(null); // => 問題なし wildcard.setValue("String"); // => コンパイルエラー // メソッドの引数に使われている T には null リテラルしか渡せない。
原型では、この制限のうち後者の制限がありません。 そのため、以下のようなコードを書いてもコンパイルエラーが発生しません(ただし、「危険な原型を使っている」というコンパイル警告は発生します)。
Holder wildcard = new Holder<String>("String"); wildcard.setValue(null); // => コンパイル可能 wildcard.setValue("String"); // => コンパイル可能
型安全でない「原型」という機能が Java に残っているのは、Java 4 以前への後方互換性を維持するためです。 原型は将来、Java の言語仕様自体からなくなる可能性も示唆されているため、なるべく利用しないべきです。
境界型ワイルドカード型
ここからは「境界型ワイルドカード」について説明します。
非境界型ワイルドカードは、
パラメータ化されたそのジェネリックス型のすべてのスーパータイプ
となる型のことでした。
「すべての」というところがポイントで、例えば List<?>
は、どんなクラス Class であっても List<Class>
のスーパータイプとなります。
一方、境界ワイルドカード型は、
パラメータ化されたそのジェネリックス型の一部のスーパータイプ
となる型です。
境界ワイルドカード型はこの「一部」というところがポイントです。 すなわち、サブタイプとなる対象のジェネリックスの制限があります。
説明のために、Object と Number と Integer のクラス階層を利用します。 この 3 つのクラスのクラス階層は以下のようになります。
このとき、境界型ワイルドカードは以下のように定義されます。
- 上限境界
- ある型のサブタイプでパラメータ化された、そのジェネリックス型のスーパータイプ
- 具体例:List<? extends Number>
- Number のサブタイプでパラメータ化された List のスーパータイプとなる型
- 下限境界型
- ある型のスーパータイプでパラメータ化された、そのジェネリックス型のスーパータイプ
- 具体例:
List<? super Number>
- Number のスーパータイプでパラメータ化された List のスーパータイプとなる型
上限境界型の性質を図にすると以下のようになります。
一方、下限境界型の性質を図にすると以下のようになります。
実際にコードを用いて、それぞれの関係を示すと以下の様になります。
Holder<? extends Number> upper1 = new Holder<Object>(); // => コンパイルエラー Holder<? extends Number> upper2 = new Holder<Number>(); // => 問題なし Holder<? extends Number> upper3 = new Holder<Integer>(); // => 問題なし Holder<? super Number> lower1 = new Holder<Object>(); // => 問題なし Holder<? super Number> lower2 = new Holder<Number>(); // => 問題なし Holder<? super Number> lower3 = new Holder<Integer>(); // => コンパイルエラー
境界型ワイルドカード型の利点
境界ワイルドカード型にはどのような利点があるでしょうか。 境界ワイルドカード型を使うと、型安全のために存在している非境界ワイルドカード型の制限を緩和することができます。
上限境界型を利用すると「メソッドの戻り値に使われている T は Object 型になる」という制限は以下のように緩和されます。
メソッドの戻り値に使われている T は Number 型になる
この制限をコードで示すと以下のようになります。
Holder<? extends Number> holder = new Holder<Integer>(new Integer(1)); Object obj = holder.getValue(); // => 問題なし(非境界ワイルドカード型の場合と同じ) Number n = holder.getValue(); // => 問題なし // 戻り値の T として Object ではなく Number が使えるようになっている holder.setValue(null); // => 問題なし(非境界ワイルドカード型の場合と同じ) Number number = new Integer(1); holder.setValue(number); // => コンパイルエラー // 引数の T に対する制限は緩和されていない
一方、下限境界型を利用すると「メソッドの引数に使われている T には null リテラルしか渡せない」という制限は以下のように緩和されます。
メソッドの引数に使われている T には Number が渡せる。
この制限をコードで示すと以下のようになります。
Holder<? super Number> holder = new Holder<Object>(new Object()); Object obj = holder.getValue(); // => 問題なし(非境界ワイルドカード型の場合と同じ) Number n = holder.getValue(); // => コンパイルエラー // 戻り値の T に対する制限は緩和されていない holder.setValue(null); // => 問題なし(非境界ワイルドカード型の場合と同じ) Number number = new Integer(1); holder.setValue(number); // => 問題なし // 引数の T に対して Number 型が利用できるようになっている
このように、境界ワイルドカード型を使うことで非境界ワイルドカード型の場合にあった制限を緩和することができます。
項目28(その1)と項目28(その2)で述べたように、この性質を使うと API の柔軟性を向上させることができます。
境界型ワイルドカードと PECS
境界型ワイルドカード型は項目28(その1)で説明した PECS と深い関わりがあります。 PECS は「Producer-Extends、Consumer-Super」という API の引数に関する原則を示す略語でした。
PECS は
Producer、すなわちオブジェクトを生成するジェネリックス引数は extends(上限境界型ジェネリックス)を使うべきである。
Consumer、すなわちオブジェクトを消費するジェネリックス引数は super、(下限境界型ジェネリックス)を使うべきある。
という原則を示したものです。
さきほど説明したように、上限境界型を使うと、メソッドの戻り値 T の使える型の制限が緩和されます。 Producer はメソッドの戻り値に T が宣言されているクラスのことであり、まさに上限境界型を使うことによって制限が緩くなります。
一方、下限境界型を使うと、メソッドの引数の T に null 以外が渡せるようになります。 Consumer とはメソッドの引数に T が宣言されているものであり、まさに下限型境界型を使うことで制限が緩くなります。
このように、境界型ジェネリックスを使うことで API の柔軟性を向上させることができ、それを原則化したのが PECS です。
もし、単に普通のジェネリックス(List<Hoge>
)などを使ってしまうと、メソッドの戻り値も引数も同じ型のものしか使えなくなります。
境界ワイルドカード型と変位について
項目23では不変・共変・反変について説明しました。 これらは、ジェネリックス型同士の継承についての性質です。
何度も述べていますが Java のジェネリックスはは不変です。
同じ型パラメータでパラメター化されたジェネリックス同士にしか継承関係はありません。
Object は String のスーパータイプですが、List<Object>
は List<String>
のスーパータイプではありません。
実は、境界型ワイルドカードとこの変位には似た関係があります。
例えば、ジェネリックスを共変にした場合、下記のような制限があります。
この制限は、実際、上限境界の場合と同じような制限であることが分かります。 (ただし、厳密にはメソッドの引数には T を使うことができ、null に限って引数に与えることができます)
一方、ジェネリックスを反変にした場合が以下の制限があります。
この制限は、下限境界の場合と同じ制限になります。 (ただし、厳密にはメソッドの戻り値に T は使えますが、Object 型となります)
このように上限境界・下限境界はそれぞれ共変・反変の制限と対応するのです。 発送を逆にすると上限境界・下限境界を使うと、一時的に変位を変えて、共変性・反変性を持たせることができるのです。
Scala と Java の対比
Scala などはクラス宣言時に共変・反変を宣言することができます。
// 通常のクラス abstract class Holder[T] { def get(): T // => 問題なし def set(value: T): Unit // => 問題なし } // T に共変性を指定(+T)したクラス abstract class HolderVariant[+T] { def get(): T // => 問題なし def set(value: T): Unit // => コンパイルエラー // 引数に T 型は使えない } // T に反変性を指定(-T)したクラス abstract class HolderCovariant[-T] { def get(): T // => コンパイルエラー // 戻り値に T 型は使えない def set(value: T): Unit // => 問題なし }
Scala では +T や -T と宣言することでクラスの変性を変えることができます。 ただし、このように宣言するため HolderVariant は常に共変であり、HolderCovariant は常に不変です。
一方、Java ではジェネリックスクラスの宣言時には変性を変えることはできません。 常に不変です。 ただし、境界型ワイルドカード型を使うことで一時的に変性をかえることができるのです。
List<Integer> integers = ArrayList<Integer>(); // => integers は不変の性質 List<? extends Number> numbers = integers // => 上限境界型に代入することで、一時的に共変のような性質をもたせることができる List<Object> objects = ArrayList<Object>(); // => objects は不変の性質 List<? super Number> numbers = ArrayList<Object>(); // => 下限境界型に代入することで、一時的に反変のような性質をもたせることができる
感想
長かったが、ようやく終わった。。。
本当はワイルドカードキャプチャあたりもちゃんと調べたかったが、いい加減先に進もうと思うのでここらへんでジェネリックスは終了。
なにはともあれ、結構時間をとって勉強したのでだいぶ理解が深まった。
次回からは Enum。