はじめに
コレクションAPIは、これまでずっとJava開発キット(JDK)の最も重要な要素の1つでした。ほとんどすべてのJavaプログラムは、HashMap
、ArrayList
、TreeSet
といったコレクションクラスを利用します。これらのクラスはそれぞれ異なる方法でデータを格納するため、たいていのJavaプログラマは、こうしたクラスがどのように動作するかや、どんな場合に利用すべきかをよく理解していました。しかし、J2SE 5.0のリリースによって、コレクションクラスの使い方はすっかり変わってしまいました。
ありがたいのは、学習曲線が緩やかになったことです。実際、J2SE 5.0のコレクションAPIは、多くの点で簡素化されています。また、J2SE 5.0で採り入れられたさまざまな変更によって、必要なコードの記述量は旧バージョンのJavaよりも少なくなっています。
コレクションに関わるJ2SE 5.0の新機能
本稿では、コレクションAPIに影響を及ぼす新機能を紹介します。具体的には、次の機能について説明していきます。
- ジェネリック型
- 拡張された
for
ループ - オートボクシング
これらの技術はコレクションAPIに影響を与えています。本稿では、この3つの技術を利用したコレクションクラスのサンプルを紹介します(ソースコードについてはダウンロードサンプルを参照してください)。ここで、新しいコレクションAPIの機能を詳しく説明する前に、ジェネリック型の基本的な部分を解説しておきましょう。
ジェネリック型
ジェネリック型は、コレクションAPIに追加された最も重要な要素です。拡張されたfor
ループやオートボクシングの機能は、ジェネリック型に強く依存しています。ある程度C++に詳しい方には、Javaのジェネリック型とは「C++のテンプレートに相当するが、多くの点でテンプレートより優れたもの」と説明しておきましょう。ジェネリック型を使うと、コレクションに特定の型を関連付けることができます。J2SE 5.0より前のJavaコレクションは特定の型に関連付けることができず、これが原因で問題がいくつか起こっていました。なぜ問題が起きるのか、次のコードを参照しながら説明します。
import java.util.*; public class BasicCollection { public static void main(String args[]) { ArrayList list = new ArrayList(); list.add( new String("One") ); list.add( new String("Two") ); list.add( new String("Three") ); Iterator itr = list.iterator(); while( itr.hasNext() ) { String str = (String)itr.next(); System.out.println( str ); } } }
これは、J2SE 5.0より前のJavaでよく見られるコレクションクラスのサンプルコードです。このクラスでは、ArrayList
を作成し、そこに3つの文字列を追加しています。しかし、このArrayList
には特定の型が関連付けられていないことに注意してください。Object
クラスを継承しているクラスでさえあれば、どんなクラスでもArrayList
に追加できます。
次に、ArrayList
から文字列を取り出すwhile
ループに注目してみましょう。
while( itr.hasNext() ) {
String str = (String)itr.next();
System.out.println( str );
}
このループはArrayList
内のすべてのアイテムに対して繰り返し処理を行います。ただし、取り出した各要素に対して必ず型変換をしなければなりません。なぜなら、このArrayList
は格納すべき型を知らないからです。この問題は、Javaのジェネリック型によって解決されます。ジェネリック型を使えば、このArrayList
に特定の型を関連付けることができます。そのため、このBasicCollection
クラスをジェネリック型を使って書き換えれば、さきほどの問題はなくなります。次に示すGenericCollection
クラスは、ジェネリック型を使って書き換えたものです。
import java.util.*; public class GenericCollection { public static void main(String args[]) { ArrayList<String> list = new ArrayList<String>(); list.add( new String("One") ); list.add( new String("Two") ); list.add( new String("Three") ); //list.add( new StringBuffer() ); Iterator<String> itr = list.iterator(); while( itr.hasNext() ) { String str = itr.next(); System.out.println( str ); } } }
ご覧のとおり、J2SE 5.0用に変更されたこのクラスは、元のコードからほんの数行しか変わっていません。最初の変更箇所は、ArrayList
の宣言を行っている行です。J2SE 5.0用のバージョンではString
型を用いてArrayList
を宣言しています。
ArrayList<String> list = new ArrayList<String>();
このコードが、どのようにして型とコレクションの名前を結び付けているかに注意してください。ジェネリック型を指定するには、コレクション名のすぐ後ろに山カッコ(<>)を記述し、その中に型の名前を指定します。
次に、反復子(イテレータ)の宣言では、String
型のためのIterator
を宣言します。このイテレータについても、ArrayList
とまったく同じようにしてジェネリック型を指定します。たとえば、次のようになります。
Iterator<String> itr = list.iterator();
この行は、Iterator
の型をString
に指定しています。これで、この反復子のnext
メソッドを呼び出すときには、もう型変換を行わずに済みます。次のように普通にnext
メソッドを呼び出すだけで、String
型が返ってきます。
String str = itr.next();
たしかにコードの記述量は減りますが、ジェネリック型は型変換を不要にするだけのものではありません。ジェネリック型を指定しておくと、ArrayList
に「サポート外の型」を追加しようとしたときにコンパイルエラーが生成されるのです。では、「サポート外の型」とは何でしょうか。たとえば、今回の例ではArrayList
がString
を受けとるように宣言したため、String
以外のクラスまたはString
のサブクラスがサポート外の型と見なされます。
サポート外の型の一例として、StringBuffer
クラスを挙げましょう。今回の例では、ArrayList
はString
のみを受けとるので、StringBuffer
オブジェクトを格納することはできません。たとえば、次の行をプログラムに追加するのは誤りです。
list.add( new StringBuffer() );
ここからがジェネリック型のすばらしいところですが、サポート外の型をArrayList
に追加するコードを書いても、ランタイムエラーにはなりません。なぜなら、あらかじめコンパイル時にこの誤りが検出されるからです。この場合は、次のようなコンパイルエラーが発生します。
c:\collections\GenericCollection.java:12: cannot find symbol symbol : method add(java.lang.StringBuffer) location: class java.util.ArrayList<java.lang.String> list.add( new StringBuffer() );
こうしたエラーをコンパイル時に検出できることは、バグのないコードを書こうとする開発者にとって非常に好都合です。J2SE 5.0より前のバージョンであれば、実行時になってClassCastException
が発生するまでこのエラーが見つからないことが多かったでしょう。どんな場合も、実行時にバグ発生の適切な条件が揃うのを待つより、コンパイル時にエラーを検出できるほうが好ましいはずです。というのも、この「適切な条件」というのは、導入が終わってこのプログラムをユーザが実行するまで発生しないことが多いからです。