[勉強会便り] Java のジェネリックス
最初の話題として、現在読んでいるテキスト「Effective Java 第2版」から Java のジェネリックスについて取り上げます。
「Effective Java 第2版」は Java を仕事に使うプログラマなら、是非読んでおきたい書籍です。
とは言っても、日々の業務で忙しかったりすると、なかなか新しい本を1冊読むことは難しく、私もこの本の初版は新人時代に読みましたが、第2版は読んでいませんでした。
第2版は初版にはない、Java5 以降に追加されたジェネリックスや列挙型についての情報が充実しています。
ジェネリックスを使うとなぜかコンパイルエラーや警告が発生して困る、という人はこの本を読むと解決策が分かると思います。
「Effective Java 第2版」でジェネリックについて解説されている項目は以下の通りです。
- 項目23 新たなコードで原型を使用しない
- 項目24 無検査警告を取り除く
- 項目25 配列よりリストを選ぶ
- 項目26 ジェネリック型を使用する
- 項目27 ジェネリックメソッドを使用する
- 項目28 APIの柔軟性向上のために境界ワイルドカードを使用する
- 項目29 型安全な異種コンテナーを検討する
今回は、この本を読んで気付いたジェネリックスをうまく扱うためのポイントを何点か紹介したいと思います。
詳細については書籍を読んで下さい。
ジェネリックスとは
まずジェネリックスとはどういうものでしょうか。
例えば、java.util.List や java.util.ArrayList といったコンテナ型を使用する場合、Java 1.4 以前であれば
List list = new ArrayList(); list.add("one"); list.add("two"); // ... String one = (String) list.get(0);
というように、取り出すときに格納されている要素の型に応じて、String型であれば String にキャストしなければいけませんでした。
これをジェネリックスを使用するとこう書けます。
List<String> list = new ArrayList<String>(); list.add("one"); list.add("two"); // ... String one = list.get(0);
最初の変数 list の宣言と、ArrayList インスタンスの生成時に指定している <String> の部分がジェネリックスの型パラメータです。
これだけ見ると、ジェネリックスは取り出すときにキャストを書かなくていいから便利、くらいにしか思いませんが、裏を返せば取り出すときにキャストを書かなくても、実行時にキャスト例外が発生しないことをコンパイラが保証してくれている、ということです。
ジェネリックスをうまく使うためには、実行時にキャスト例外が発生しないとコンパイラが判断できるための情報をすべてコードに埋め込んであげないといけません。
このことをちゃんと意識することがジェネリックスをうまく利用するためには重要です。
Web開発の現場などでは、Perl や Python、Ruby といったLL(Lightweight Language)などの動的型付言語(変数の型を指定しない言語)が注目されていますが、ジェネリックスは静的型付言語である Java の静的な型チェックをより強力する手法ですので、ある意味 LL とは真逆な方向です。
ソースコードに情報を付加することで、今まで実行するまで分からなかった型チェックによるエラーがコンパイル時に判定できるようになったということです。
ジェネリック型を使わないと警告
Java5 以降では、型パラメータを指定せずに java.util.List などのジェネリック型を使用するとコンパイル時に警告が発生します。
これは、型パラメータを指定しない型(原型といいます)を使用すると、値を取り出すときのキャストがチェックできないので、実行時に例外がでちゃうかもしれないということです。
ジェネリック型と配列との違い
Java 1.4 までであれば、キャストが面倒なので List ではなく配列を使ったということがあったかもしれません。(というか、私はありました。。。)
ジェネリック型とちがい、配列は実行時の型安全性をコンパイラが保証することはできません。
例えば、次のようなコードを見て下さい。
String[] strArray = {"one", "two"}; Object[] objArray = strArray; objArray[0] = Integer.valueOf(1); // 実行時例外: ArrayStoreException
上記のコードはコンパイルが通りますが、実行時に例外が発生します。
これがコンパイルを通るのは、String[] が Object[] のサブタイプであるためです。この性質を共変(covariant)と言います。
一方、ジェネリック型は共変ではなく不変(invariant)です。List<String> は List<Object> のサブタイプではありません。
List<Object> の変数に List<String> の変数を代入しようとするとコンパイルエラーになります。
List<String> strList = new ArrayList<String>(); List<Object> objList = strList; // コンパイルエラー: 互換性のない型
この性質は少し直感に反するように思うかもしれませんが、この性質により ArrayList<String> のインスタンスが List<Object> の変数に代入されて String 以外の要素を代入されるといった実行時例外を防ぐことができます。
まとめると、キャストが不要な点は配列もジェネリックスも同じですが、ジェネリックスの方がより安全です。
ワイルドカード
List<String> は List<Object> のサブタイプではないということですが、では、要素の型にかかわらず List オブジェクトを受け取るメソッドを定義するときにはどうすればよいのでしょうか?
原型である List を使用すると、コンパイル時に警告が出てしまいますし、List<Object> とするとコンパイルエラーになります。
こういう場合は、型パラメータに ? を使用することができます。? をワイルドカードと呼びます。List<String> や List<Object> は List<?> のサブタイプになります。
void printAll(List<?> list) { for (Object o : list) { System.out.println(o); } }
上記の printAll メソッドには、List<String> や List<Integer> などを渡すことができます。この List<?> の部分を List<Object> と書いてしまうと、この printAll メソッドには List<Object> 型しか渡せなくなってしまいます。
ワイルドカードには境界型ワイルドカードと非境界型ワイルドカードと呼ばれる2種類のものがあります。上記の例は非境界型ワイルドカードです。境界型ワイルドカードでは要素の型のスーパータイプを指定することができます。
例えば、List<? extends java.util.Date> と宣言した場合、java.util.Date のサブタイプである java.sql.Timestamp などの List を受け取ることができます。
例えば、次のようなメソッド定義が可能です。
void printDateList(List<? extends Date> dateList) { for (Date d : dateList) { DateFormat format = new SimpleDateFormat("yyyy/MM/dd"); System.out.println(format.format(d)); } }
終わりに
今回紹介したポイントを押さえると Java のジェネリックスをうまく利用できるのではないかと思います。
これからも、勉強会で出てきた話題の中から役立ちそうなものを紹介していきたいと思います。お楽しみに。