|
Generics |
||||||||||||
|
||||||||||||
Generics はすでに雑誌などで解説記事が書かれているのでご存知のかたも多いかと思います。 Generics というのは C++ のテンプレート機能のような機能で、ひとことでいえばクラス (インタフェース)、メソッドのパラメータ化ということができます。 といってもよく分からないですね。 それでは実際に例を示しましょう。 コレクションは要素を Object クラスのオブジェクトとして保持するために、使うときにはいちいちキャストを行わなくてはいけません。
最後の行は非常に醜くなってしまいます。 Generics を使うとつぎのようになります。
とてもすっきりしました。 1 行目の <Integer> というのが Generics の表記です。これはどういう意味かというと、<Integer> で修飾された List オブジェクトは保持する要素がすべて Integer オブジェクトになるということを示しています。このためキャストがいらなくなっているのです。 見ためにはキャストがいらなくなっただけですが、プログラムの保守という観点からするともっと重要な点があります。 たとえば次のコードは問題なくコンパイルできます。
しかし、これを実行すると当たり前ですが ClassCastException が発生してしまいます。
ようするにこの間違いはプログラムを実行しない限り発見されないので、デバッグが面倒になります。コンパイル時にチェックできれば、あっというまに修正できるのですが... これを Generics を使用すればつぎのように書くことができます。
これをコンパイルします。
コンパイル時にコンパイルエラーが出るため、まちがった要素を保持させようとしていたことがすぐわかります。ちょっとした違いのような気もしますが、これが保守性には大きな違いになるのです。 Generics は JCP で標準が策定されています。JSR-014 なのでずいぶん早い時期に JCP に登録されたのですが、なかなかまとまりませんでした。やっと Tiger でコアにとりこまれます。 Generics の仕様は JCP からダウンロードできます。 JSR-014 Add Generic Types To The Java Programming Language http://jcp.org/en/jsr/detail?id=14 なるべく、最新の情報を追いかけていこうとは思っていますが、もし違っている部分がありましたらメールください。
|
|
||||||||||||||||
Generics の使い方が簡単だということは一番はじめの例から分かると思います。ここでは、いくつか例を示しながら、もうちょっと使い方を見ていきましょう。 まずはコレクション関連で使ってみましょう。コレクションは Generics によってもっとも変化した部分なので、ここが抑えられればもう大丈夫。
List はじめの例は List インタフェースと ArrayList クラスの例です。単に文字列の連結をやっています。今までの書き方では次のようになります。ちなみにクラス名の WO は without のことです。
これを Generics で書くと次のようになります。赤の部分が Generics の宣言部分です。
リストに要素を追加するときにはパラメータの型 (ここでは String) であるかどうかチェックされます。逆に、Generics が使えるときに、Generics を使わないでコンパイルすると
と表示されます。-warnunchecked オプションをつけてコンパイルすると
と表示されて、型チェックされないことが警告されます。 get するときにキャストがいらないのは、前述の通り。
Iterator 次は Iterator インタフェースを使用した例です。今までの記述方だと次のようになります。
Collection#iterator メソッドは戻り値がパラメータ化された Iterator オブジェクトになります。Generics で書きなおすと
キャストがいらないというのは、ずいぶん楽になります。ほんとに。
入れ子パラメータ さて、次は 2 重リストを作ってみましょう。リストの要素がリストというリストです。次のプログラムはトランプのカードを作るという例です。
要素を取り出すのに 2 回もキャストする必要があります。面倒くさいですし、分かりにくいですね。 これを Generics で書くと次のようになります。2 重リストなので、パラメータの中にパラメータ化したクラスが入るという入れ子構造になっています。
Generics を使った定義の部分がちょっと見にくいかもしれません。慣れればナンでもありません ^^;; 宣言は 1 回だけですが、get するのは何度もあるかもしれないのです。 get の部分の簡潔さは感動的です。
複数パラメータ パラメータの入れ子ができることは分かりましたので、複数のパラメータの例を次に示しましょう。 パラメータを 2 つ使うものにマップがあります。マップはご存知のようにキーと値のペアになりますが、この両方ともパラメータとすることができます。 まずは今までの書き方。
Generics で書きかえたものが次です。複数のパラメータがある場合はカンマで区切って書きます。
Generics を使うと put メソッドで型チェックが行われます。キーと値の両方ともチェックされます。 また、get するときもキーの型チェックが行われます。 どうですか、Generics を使うのもそんなに面倒ではないことがお分かりだと思います。次は少し文法的なことを調べてみましょう。
|
|
|||||||||||||||||
今まではあまり考えることなく Generics を使ってきましたが、いろいろと気になるところもあります。それらをチェックしてみましょう。
なにをパラメータとできるか 今までの例はみなパラメータとしてクラスもしくはインタフェースを使用してきました。それじゃ、こんなのは
パラメータに使用できるのはあくまでも、クラスかインタフェースだけでプリミティブ型は使用できません。もちろん、ラッパークラスを使用することはできます。 もちろん、勝手にパラメータの数を増やすこともできませんList インタフェースのパラメータは 1 つだけなので、次の例は間違いです。
パラメータ化されたクラスは元のクラスと区別されるのか C++ のテンプレートはパラメータ化されると、違うクラスとして認識されます。Java ではどうでしょう。 次の例で確かめてみましょう。
さっそく実行。
あら、一緒だ。 それじゃ、こんなこともできるのでしょうか。
これをコンパイルすると、やっぱりダメでした ^^;;
クラスは同じなのに代入できないというのも変な感じですが、まあ当たり前でしょう。 それじゃ、パラメータ化していないオブジェクトに代入してみましょう。ちなみにパラメータ化していないものを Raw タイプというそうです。
これをコンパイルすると
パラメータ化されたオブジェクトをパラメータ化していないオブジェクトに代入することは OK のようです。逆は警告が出ているので、いまいちお勧めできません。 パラメータ化していないオブジェクトに代入しても型チェックはちゃんと行われているようです。また、キャストも使う必要がないようです。 まぁ、紛らわしいのでこういう使い方はやめておいた方が懸命だとは思いますが。
キャストはできるか 代入ができたので、キャストも試してみましょう。
多分、最後の List<String> はダメだと思いますが、List へのキャストはどうでしょうか。さっそく、コンパイル。
List へのキャストは大方の予想通り大丈夫です。List<String> へのキャストはやっぱりダメですね。
パラメータ化されたクラスを派生できるか パラメータ化されたとしても、クラスとして扱われるようなので、こんなことができるかどうか試してみたくなりました。
ようするにパラメータ化されたクラスを派生できるか、もしくはパラメータ化されたインタフェースをインプリメントできるかということです。 コンパイルしてみると、次のようになりました。
派生させた部分に関してはコンパイルエラーは出ていないようです。A は普通のクラスのように見えますが、ちゃんと型チェックやキャストが不要になっています。10 行目と、11 行目をコメントアウトして実行してみるとちゃんと実行できました。 インタフェースでもやってみました。よく分からないのが next メソッドの戻り値です。元々の Iterator インタフェースの場合の戻り値は Object 型です。でも、パラメータ化したらパラメータの型になるはずだからです。
とりあえず、このままでコンパイルしてみましょう。
やはり、next メソッドの戻り値はパラメータの String になるようです。これを変更して実行してみると
とちゃんと実行できました。 ただし、このようにパラメータ化されたクラスやインタフェースを派生・インプリメントしてもあまり意味がありません。やるんだったらパラメータ化したまま定義したほうがいいと思います。というわけで、次からは実際に自分でパラメータ化したクラスを作ってみましょう。
|
|
|||||||||||
今までは使い方ばかりだったのですが、ここからは Generics を利用してクラス、インタフェース、メソッドを作ってみましょう。
クラスのパラメータ化 まずはクラス、インタフェースからです。クラス、インタフェースの場合、クラスの定義の部分でクラス名、インタフェース名の直後に <T> のようにパラメータをつけて宣言します。別にパラメターの型名は T でなくてナンでもいいのですが、慣例的にはアルファベットの大文字 1 文字を使用することが多いです。
クラスの中ではパラメータの型は普通の型として使用することができます。そのため、プロパティやメソッドの引数、メソッドの戻り値、テンポラリ変数などに使用することができます。 インタフェースでも同様です。
派生クラスやインタフェースのインプリメントもできます。
簡単でしょ。 実をいうと驚いたことにワイルドカードがつかえるのです。ワイルドカードは ? で表わします。
<? extends T> というのは「T の派生クラスだったらなんでもいい」ということです。<? super T> という書き方は T のスーパクラスだったらなんでもいいということです。両方ともインタフェースも使うことができます。 とはいうものの List<? extends T> を List<T> にキャストするとコンパイル時に警告されます。 また、単に <?> と書いてすべてのクラスという書き方もできますが、これはパラメータ化したメソッドで使われることが多いので、また後で説明します。 ちょっと試してみましょう。
A クラスの派生クラスの B。その派生クラスの C を用意して、Everything クラスの get/set メソッドをコールしてみるという例です。 これをコンパイルすると
エラーは 2 つで、警告が 1 つ。警告は前述したように List<? extends T> を List<T> にキャストしたことによります。 エラーは List<? extends B> が期待されていたところに List<A> だったというのが 1 つ。A は B のスーパクラスなのでこれはだめです。 次が List<? super B> が期待されていたところに List<C> だったということです。C は B の派生クラスなのでやっぱりダメですね。
メソッドのパラメータ化 さて、次はメソッドの方です。クラスがパラメータ化されていなくても、メソッドだけをパラメータ化することができます。 メソッドをパラメータ化するには public や private や static などの後、戻り値の前に <パラメータ名> を記述します。 クラスの場合はクラス名の後だったのに対し、メソッドではメソッド名の前になります。 次の例は java.util.Collections クラスの fill メソッドの定義です。
T がパラメータとなる型です。メソッドの中では T は普通の型のように使えるのはクラスのときと同様です。 java.util.Collections クラスや java.util.Arrays クラスのように static メソッドばかりのクラスはインスタン化しないので、このようにメソッドをパラメータ化することが多いようです。 先ほどと同様にワイルドカードを用いることもできます。次の例は同じく Collections クラスの copy メソッドです。
T の派生クラスは T のスーパクラスにはコピーできるということです。 単なるワイルドカードも使えます。この場合はどんなクラスでもいいので、戻り値の前のパラメータ表記は必要ありません。 ワイルドカードの例として Collections クラスの reverse メソッドです。リストの並び順を変えるだけなので、List の要素はどんなクラスのオブジェクトでもいいわけです。
メソッドだけをパラメータ化することはあまりないかもしれませんが、static メソッドだけのユーティリティクラスなどで重宝しそうです。 |
|
|||||
今まで例にした Generics の使い方はすべてクラス/インタフェースに対する Generics でした。というのもメソッドの方は定義の仕方を説明してから出ないと、使い方が分かりにくいと思ったからです。 前章でパラメータ化したメソッドの書き方を示したので、さっそく使ってみましょう。 パラメータ化されているメソッドをコールする場合はメソッド名の前に <クラス名 or インタフェース名> を書くようにします。クラスの場合は後ろだったのですが、メソッドは逆に前です。 まずは、先ほどの Collections#replaceAll メソッドを使ってみましょう。
赤の部分がパラメータ化したメソッドです。メソッド名の前に <Integer> をつけるだけで後は普通のメソッドと同様にコールすることができます。 Collections#copy も使って見ましょう。
クラスの時は単にスーパクラスを使ってみたので、ここではインタフェースを使ってみましたが、問題なく使えています。 最後は Collections#reverse です。単にワイルドカードを使用したメソッドはパラメータの表記を行っていませんでした。使うときも同様で、まったく普通のメソッドと同じように使うことができます。
|
|
||||
プログラムを作る上ではあまり関係ないかもしれませんが、Generics を導入することによっていくつか言語仕様やクラスファイルの構造が変更されています。
シフト演算子と Generics Generics が入れ子になっていると List<List<String>> なんてふうに >> というのが出てくることがあります。でも、よく考えたらこれはシフト演算子ですね。3 重に入れ子にしたら >>> となってしまいます。これもシフト演算子です。 Generics が導入されたため言語仕様が変更されて、>> や >>> といったシフト演算子と Generics が区別されるようになりました。
戻り値だけ違うメソッドのオーバライド 今までは戻り値だけ違うメソッドを派生クラスがオーバライドすることはできませんでした。 たとえば、つぎのようなプログラムはどうでしょう。
Y クラスでは foo メソッドの戻り値が X クラスのそれと異なり Integer クラスになっています。これを従来の javac でコンパイルすると、
当然のごとくエラーが出ます。ところが Generics を導入することで、スーパクラスのメソッドの戻り値のクラスの派生クラスであれば、派生クラスでメソッドをオーバライドしていいようになりました。 先ほどの例だと、Integer クラスは Number クラスの派生クラスなので、コンパイルできることになります。
|
|
|||||
J2SDK だと分からなくなってしまいますが、Early Access 版だと java や javac は Windows ならバッチファイル、UNIX だとシェルスクリプトで実現されていました。 たとえば javac.bat と java.bat を見てみると、javac の方はいろいろと変更が加えられています。しかし、java の方は JAR ファイルが 1 つ追加されているだけです。 Windows 版の java.bat で実際に Java を動作させる部分は次のようになっています。
-Xbootclasspath/p: というのはコアのライブラリを読み込む前にここで指定している JAR ファイルを読み込ませるためのオプションです。 ようするに gjc-rt.jar という JAR ファイルを読み込んでいるだけで、JVM は全然変更されていないのです。 ということは javac だけで全部行っている??? それじゃ、解析してみましょう。こういうときに役立つのが逆コンパイラ。今回は Jad を使ってみました。 次の例を Jad で逆コンパイルをして見ました。
Jad の結果はデフォルトでは拡張子が jad となります。逆コンパイルの結果です。
普通の書き方に戻っています !! 要するに、javac でコンパイルするとき、型チェックを行うようにして、Generics の部分は以前の書き方に戻してくれているだけのようです。すごいと思いません。 この解析は Early Access 版で行いましたが、J2SDK でも同じ結果になりました。JVM が変更されていないのはたしかなようです。 ただし、クラスファイルの構造は変化しています。クラスやメソッドの定義部分にはアトリビュートという領域があるのですが、ここに Generics の情報が記入されるようになっています。
|
|
|||||
Tiger ではコアライブラリが Generics を使って書き直されています。 どこが書きかわっているのでしょうか。少し調べてみましょう。 調べるのに必要なのは J2SDK, SE 1.5 に付属しているソースです。 ソースから Generics を使用しているものを抜き出すのは大変です。そこで、Early Access 版に付属するソースから変更されているものを探し出し、それが J2SDK でどうなっているか調べてみました。 変更されているパッケージは次の通りです。
特に変更の大きい java.lang, java.lang.reflect, java.util について調べてみましょう。 まず java.lang パッケージからです。
java.lang.lang パッケージ、java.lang.reflect パッケージ この 2 つのパッケージの変更はリフレクションに関する部分の変更です。 まず Class クラスです。 Clas クラスはパラメター化されるようになりました。したがって、次のような書き方ができます。ただし、Object#getClass メソッドの戻り値は Class<? extends Object> となっているのでキャストが必要です。
newInstance メソッドの戻り値が自動的にパラメータ化されたクラスになるのでキャストが必要なくなります。 同様に getConstructor メソッドも戻り値が Constructor<T> になります。 この結果、Constructor クラスもパラメータ化されています。やはり、newInstance メソッドの戻り値がパラメータのクラスになるので、キャストが要らなくなります。
java.util パッケージ 一番、変更が大きいのが java.util パッケージのコレクション関連のクラスです。 基本的に、Collection インタフェースの派生クラス、Map インタフェースの派生クラスはすべてパラメータ化されています。Collection インタフェースは
Map インタフェースが
となっています。Iterable インタフェースというのは新しく導入されたインタフェースで、拡張 for 文で使用されます。 また、Iterator インタフェースもパラメータ化されています。 Collections クラスと Array クラスはパラメータ化されてはいませんが、多くの static メソッドがパラメータ化されています。
|
|
||||
Generics 使いだすと、今までの苦労はなんだったんだというふうになります。軽いカルチャーショック状態です ^^;; ところが、使い出すともうやめられません。今までの生活には戻れない体になってしまいました。 たまに、J2SE 1.4 とかで書くときに、思わず Generics で書いていて、「これはいけない、もどさなくては」ということもしばしば。 今回は C++ のテンプレートとの比較なぞは行いませんでしたが、参考文献をあげましたので、そちらもご参照ください。JavaWorld の記事はかなり古いので現状の Generics とは食い違っている部分もあります。 なにはともあれ、Generics は便利なので、ぜひご活用を。
今回使用したサンプルはここからダウンロードできます。 参考
(Oct. 2003) |
|