Effective Javaを読むチャレンジ-項目28その3-

明示的型パラメータ

サンプルのunionメソッドはなぜJava SE 7以前ではコンパイルエラーになったのでしょうか。明示されていない型を引数としてメソッドを実行するとき、コンパイラは引数の型について型推論を行います。すべての引数の型がSet<Number>に変換できなかったため、コンパイルエラーとなったのだと思います。本書でも言及しているとおり、型推論の仕様は本当に複雑で、全然理解できませんでした・・・。ちなみにその言語仕様はこちら。

Java SE 7の言語仕様 §15.12.2.7

ではJava SE 8ではこの型推論はどう変わったのでしょうか。Java SE 8では言語仕様に「型推論」の章が新しく追加されました。上記の「§15.12.2.7」の内容もそちらの章に移されています。ラムダ式などができたからでしょうか。

Java SE 8の言語仕様 §18.5.2

この「§18.5.2」の最後に、「List<Number> ln = Arrays.asList(1, 2.0);」というサンプルを使って型推論がどう行われるかざっくり説明してくれています。何やかんや論理を展開すると、asListメソッドの戻り値の型がList<Number>になるのは自明の理、ということのようです。最後の注釈でこの推論戦略はjava SE 7以前ではエラーになるだろうと書かれているので、この説明の内容がサンプルのunionメソッドがjava SE 8以降コンパイルされるようになった理由を知る第一歩と思います(この二歩目を踏み出すことができるのかどうかがレベルアップへの分かれ道ですね・・・)。

ということで、型推論の理論を学ぶことはとりあえず放棄し、エラーの解決方法を考えます。このエラーを解決するためには、戻り値の型EがSet<Number>であればよいのです。unionメソッドを次のようにすればエラーが解消されます。

こんな修正する人はいませんね。何のためのジェネリックスメソッドだという話です。こんな危険な使えないコードを書かなくても、Javaにはコンパイル時に上のように型パラメータを明示的に指示するための記述があります。それが明示的型パラメータです。

明示的型パラメータを使うと、引数の型推論に使用する型を指示することができます。その書き方は以下のようになります。

「メソッドが定義されているクラス名.<型名>メソッド呼び出し」となります。この1行の修正でunionメソッドのコンパイルエラーは解消されます。ちなみに自クラスのメソッドであってもクラス名の省略はできません。

再帰型境界のサンプル

次のサンプルは項目27のmaxメソッドです。このメソッドは項目27の再帰型境界という用語の説明で使われたメソッドですが、本サイトではまったく触れてませんでしたね・・・完全に見落としてました。

再帰型境界というのは、型パラメータが自分自身を使った式で制限されることを言います。本書のサンプルを見てみましょう。

このメソッド宣言は「T自身と比較可能なTに関して、Tのリストを引数とし、Tを戻り値の型とするmaxメソッド」となります。この「T自身と比較可能なTに関して」という部分が再帰型境界を言い表しています。それではこのmaxメソッドについて、ワイルドカードを使用して柔軟性を向上することを検討したいと思います。

PECSに従うと、まず引数のTは「Iterator<T> i」を生産するために使用されているので、「? extends T」となります。

次はComparable<T>のTですが、これは型パラメータがもつ型パラメータについてのPECSですので、maxメソッドではなくComparableについて考えます。Comparableのインスタンスはtです。そのtのcompareメソッドの引数であるresultは生産を行うのでしょうか、消費を行うのでしょうか。少なくとも生産は行いませんので、消費を行うということになります。本書の「比較可能なものは常に消費者」という言葉はそういう意味です。よってComparable<T>のTは常に「? super T」となります。結果として、maxメソッドの宣言はこうなります。

ところが、この修正を行うとメソッド本体の方でコンパイルエラーが発生します。それは「Iterator<T> i = list.iterator();」の部分です。すぐお分かりかと思いますが、引数のlistが境界ワイルドカードに変わったためにその型パラメータはTの何らかのサブタイプになります。それにより「Iterator<T>」に型変換できなくなりました。このエラーを解消するためには、「Iterator<T>」を「Iterator<? extends T>」として、Tの何らかのサブタイプを許容するようにします。

型パラメータとワイルドカードには二重性がある

つまり2つは同じ性質を持っているということですね。その性質を利用してこんなことができますよ、という話です。まず次の2つのメソッド宣言のどちらが好ましいかとはじまります。

この2つのメソッド宣言、本書は単純で好ましいという理由で2つ目を推しています。という流れで非境界型ワイルドカードを使った方のメソッドを次のように実装しています。

この章で何度も出てきたように、非境界型ワイルドカードのリストにはnull以外の値は挿入できません。非境界型パラメータを使ったメソッドであれば、このメソッド本体のコードで動作しますが、非境界型ワイルドカードをそのまま使おうとするとコンパイルエラーになります。そこで、非境界型ワイルドカードのままでコンパイルエラーを起こさずに実装する方法があるということです。

そのために使用するのがワイルドカードをキャプチャするヘルパーメソッドです。それを使ったコードは以下となります。

非境界型ワイルドカードの引数を非境界型パラメータの引数をもつ別のメソッドに渡しています。非境界型ワイルドカードはどんな型か分からないがどんな型でもいい場合に使用します。つまり何かの型であることに代わりはないので、非境界型パラメータの引数として渡せます。キャプチャ側からすると引数の非境界型パラメータはList<E>であり、型が分かっているので型安全に値を入れることができるということですね。

実はこのswapメソッドですが、次のように書いてもコンパイルされます。

原型を使用しちゃっているので本書の思想からは大きく外れます。また、そのせいで警告がいくつも出ます。でもこれ、Javaソースコードで行っている実装です。java.util.Collections#swapメソッドですね。著者がこのやり方が許せなかったのか、こうしておけばよかったと思っているのかは分かりませんが、型パラメータとワイルドカードの二重性を利用すれば、ワイルドカードを引数に持つ単純なAPIにできる、つまり複雑なジェネリックメソッドを公開する必要がなくなり、原型を使用せず型安全にワイルドカードを使ったAPIを実装できるということに代わりはありません。上記のコードは悪い例ということです。

長かったですが項目28は以上です。まさにクライマックスという感じでした。

広告
  • LINEで送る