プログラミングでは、データを集め、集められたデータについて何らかの操作を行うことがよくあります。Javaには、こうしたことに対応するため、「コレクション・フレームワーク」という機能があります。「コレクション」とは、オブジェクトをまとめているオブジェクトです。
コレクション・フレームワークには、次のような機能があります。
コレクションには、6つの基本的なインターフェースと、その実装クラスがあります。
ここでは、List, Set, Mapの3つのインターフェースを見てみましょう。
Listは、図2.1[List]のような、オブジェクトが順番に並んだコレクションです。
java.util.Listはインターフェースであり、これを実装したクラスには
などがあります。これらのクラスはそれぞれ実装方法が異なっており、状況に応じて使い分けることができます。
リスト4-1は、ファイルの内容を1行ずつArrayListクラスのインスタンスに格納するメソッドです。
// リスト 4-1 1 public List<String> readFile(String fileName) { 2 List<String> list = new ArrayList<String>(); 3 try { 4 FileReader file_reader = new FileReader(fileName); 5 BufferedReader buffered_reader = new BufferedReader(file_reader); 6 String line; 7 while ((line = buffered_reader.readLine()) != null) { 8 list.add(line); 9 } 10 buffered_reader.close(); 11 file_reader.close(); 12 } catch (FileNotFoundException e) { 13 e.printStackTrace(); 14 } catch (IOException e) { 15 e.printStackTrace(); 16 } 17 return list; 18 }
リスト4-1では、2行目で
List<String> list = new ArrayList<String>();
のように、ArrayListクラスのインスタンスを生成しています。このreadFileメソッドでは、ファイルの内容を1行ずつ取りだしています。取り出したデータは、8行目でListインターフェースのメソッドList#add(String)を使い、オブジェクトlistに追加します。つまりreadFileメソッドでは、ファイルのすべての行をlistに格納し、最後にこのlistを返しているのです。
リスト4-1の2行目について、もう少し解説を加えましょう。
List<String> list = new ArrayList<String>();
普通なら、次のように書くところです。
ArrayList<String> list = new ArrayList<String>();
しかし、先に述べたように、ArrayListはListインタフェースを実装しています。
public class ArrayList implements List { …… }
この場合、ArrayList型のインスタンスは、List型も持っていることになります。よって、次のように記述できます。
List<String> list = new ArrayList<String>();
では、ArrayListの代わりに後述するLinkedListを使うようにしたとしましょう。ArrayListと同様に、LinkedListもListインタフェースを実装しているので、次のように書けばよいのです。
List<String> list = new LinkedList<String>();
ArrayListを使ったプログラムをJ2SE 1.4で書くと、次のようなパターンになります。
List l = new ArrayList(); l.add("1000"); …… String str = (String)list.get(0);
addメソッドではString型である"1000"というオブジェクトを追加しています。このメソッドの引数はObject型です。JavaのすべてのクラスはObject型を継承しているので、つまりaddメソッドにはどのような型のオブジェクトでも追加できるということになります。
最後の行のように、getメソッドでオブジェクトを取得しています。このメソッドの返値もObject型なので、(String)のようにキャストを行っています。
ところが、この方式では、listインスタンスにどのような型のオブジェクトでも追加できてしまいます。そのため、String型以外のオブジェクトがlistに追加されていると、最終行のキャストのところでエラーになってしまいます。
このエラーは、コンパイル時ではなく、実行時に検出されることになります。実行時エラーの原因追及は、なかなかやっかいな作業です。
こうしたエラーを防ぐために、J2SE 5.0からはGenericsという機能が追加されました。
Genericsを使うと、次のようにプログラムを記述できます。
List<String> list = new ArrayList<String>(); list.add("1000"); …… String str = list.get(0);
1行目の2カ所で登場する"<String>"の部分がGenericsです。この行では、Listで管理するオブジェクトをString型に限定しています。それ以外の型のオブジェクトが追加されると、エラーになります。つまり、listインスタンスに含まれるのはString型だけであることが保証されるのです。このおかげで、最終行のgetメソッドではキャストが必要なくなります。
Genericsを使うことによって、コレクションの定義が明確になり、実行時エラーが起こる可能性をかなり減らすことができます。
Listインターフェースを実装したクラスのなかでは、java.util.ArrayListがよく使われています。しかし、ArrayListのインスタンスに対して、頻繁にオブジェクトの挿入や削除を行うなら、ArrayListではなくjava.util.LinkedListを使った方が良いでしょう。このクラスは図2.2[LinkedList]のように「ダブルリンクトリスト」という手法を使って実装されており、オブジェクトの挿入や削除が頻繁に起こる場合、効率が良くなるからです。
リスト4-1でjava.util.ArrayListをjava.util.LinkedListに変えるには、2行目の部分を
List<String> list = new LinkedList<String>();
とするだけです。他の部分は変更しなくても構いません。
Javaはマルチスレッドに対応したプログラミング言語なので、複数のスレッドを同時に動作させることが可能になります。
1つのオブジェクトに、2つのスレッドがアクセスするとしましょう。1つのスレッドでは、そのオブジェクトを削除しようとしています。別のスレッドでは、オブジェクトを内容を取得しようとしています。この2つのスレッドが同時に実行され、オブジェクトへのアクセスが同時に行われた場合、矛盾が生じる可能性があります。
そこで、ひとつのオブジェクトに、同時にはアクセスできないようにする工夫が必要です。これを「同期化」と言います。ただし、同期化させるとプログラムの処理が遅くなります。
ArrayListやLinkedListは同期化されていないので、マルチスレッドで使うときには動作が保証されません。同期化するには、リスト4-2のようにjava.util.CollectionsクラスのメソッドCollections#synchronizedList(List)を使います。
// リスト 4-2 List<String> newList = Collections.synchronizedList(list);
java.util.Vectorは、Java 2 SDKが登場する前から存在しており、ArrayListによく似た機能を持っています。ArrayListとは違って、同期化もされています。Java 2 SDK以降でのVectorは、java.util.Listインターフェースを実装するように変更されました。
Setは、オブジェクトの重複がないコレクションです。数学での「集合」をモデル化しています。
java.util.Setはインターフェースであり、これを実装したクラスに
があります。
リスト4-3は、HashSetクラスのインスタンスにあらかじめオブジェクトを登録しておき、指定したオブジェクトがコレクションに存在するかどうか確かめるプログラムです。
// リスト 4-3 1 import java.util.Set; 2 import java.util.HashSet; 3 4 public class SetTest { 5 private Set<String> set; 6 7 public SetTest() { 8 set = new HashSet<String>(); 9 10 // あらかじめデータを登録する 11 set.add("10000"); 12 set.add("20000"); 13 set.add("30000"); 14 } 15 16 public void check(String number) { 17 if (set.contains(number)) { 18 System.out.print(number); 19 System.out.println("はあります"); 20 } else { 21 System.out.print(number); 22 System.out.println("はありません"); 23 } 24 } 25 26 public static void main(String[] argv) { 27 SetTest test = new SetTest(); 28 test.check("10000"); 29 test.check("10001"); 30 test.check("20000"); 31 } 32 }
// リスト 4-3 の実行例 C:\Home> java SetTest 10000はあります 10001はありません 20000はあります
8行目のコンストラクタで生成されるインスタンスの型は、java.util.Setです。11行目から13行目でオブジェクトを登録し、17行目では引数で指定したオブジェクトが存在するかどうか確かめています。
ArrayListと同じように、HashSetも同期化されていないので、マルチスレッドで使うときには動作が保証されません。同期化するには、リスト4-4のように、java.util.CollectionsクラスのメソッドCollections#synchronizedSet(Set)を使います。
// リスト 4-4 Set<String> newSet = Collections.synchronizedSet(set);
java.util.Setを継承しているインターフェースに、java.util.SortedSetがあります。java.util.SortedSetは、内部でオブジェクトの順番を保持することを保証します。つまりコレクションにオブジェクトの追加や削除を行っても、その中のオブジェクトは自動的に並びかえられます。
java.util.SortedSetを実装しているクラスには、java.util.TreeSetがあります。
Mapは、「キー」と「値」の組合せでオブジェクトが格納されるコレクションです。ちょうど辞書のようなイメージになります。
java.util.Mapはインターフェースであり、これを実装したクラスには
などがあります。これらのクラスはそれぞれ実装方法が異なっており、状況に応じて使い分けることができます。
リスト4-5は、HashMapクラスのインスタンスにあらかじめオブジェクトを登録しておき、指定したキーがコレクションに存在するかどうか確かめ、キーが存在するならそのキーに対応する値を出力するプログラムです。
// リスト 4-5 1 import java.util.Map; 2 import java.util.HashMap; 3 4 public class MapTest { 5 6 private Map<String, String> map; 7 8 public MapTest() { 9 map = new HashMap<String, String>(); 10 map.put("10000", "福沢諭吉"); 11 map.put("5000", "樋口一葉"); 12 map.put("2000", "紫式部"); 13 map.put("1000", "野口英世"); 14 } 15 16 public void check(String key) { 17 if (map.containsKey(key)) { 18 String value = map.get(key); 19 System.out.print(key); 20 System.out.print("円札は"); 21 System.out.print(value); 22 System.out.println("です"); 23 } else { 24 System.out.print(key); 25 System.out.println("円札はありません"); 26 } 27 } 28 29 public static void main(String[] argv) { 30 MapTest test = new MapTest(); 31 test.check("500"); 32 test.check("2000"); 33 test.check("10000"); 34 } 35 }
// リスト 4-5 の実行例 C:\Home> java MapTest 500円札はありません 2000円札は紫式部です 10000円札は福沢諭吉です
9行目のコンストラクタで生成されるインスタンスの型は、java.util.Mapにしています。10行目から13行目でキーと値のペアを登録し、17行目では引数で指定したキーが存在するかどうか確かめています。そして18行目で引数をキーにして値を取り出します。
HashMapもやはり同期化されていないので、マルチスレッドで使うときには動作が保証されません。同期化するには、リスト4-6のようにjava.util.CollectionsクラスのメソッドCollections#synchronizedMap(Map)を使います。
// リスト 4-6 Map<String, String> newMap = Collections.synchronizedMap(map);
java.util.Mapを継承しているインターフェースに、java.util.SortedMapがあります。java.util.SortedMapは、内部でキーの順番を保持することを保証しています。つまり、コレクションにオブジェクトの追加や削除を行っても、その中のオブジェクトはキーの順序で自動的に並びかえられます。java.util.SortedMapを実装しているクラスには、java.util.TreeMapがあります。
java.util.Hashtableは、Java 2 SDKが登場する前から存在しており、HashMapによく似た機能を持っています。HashMapとは違って、同期化もされています。Java 2 SDK以降でのHashtableは、java.util.Mapインターフェースを実装するように変更されました。
コレクションに含まれているそれぞれのオブジェクトに対して、繰り返しの処理をするには、java.util.Iteratorを使います。図2.5[Iterator]のようなイメージになります。
まずは、Iteratorを使ったサンプルを見てみましょう。リスト4-7は、リスト4-1のプログラムに、List型のオブジェクトに格納されたファイルの中身をIteratorを使って出力する機能を加えたものです。
// リスト 4-7 1 import java.util.List; 2 import java.util.ArrayList; 3 import java.util.Iterator; 4 import java.io.FileReader; 5 import java.io.BufferedReader; 6 import java.io.FileNotFoundException; 7 import java.io.IOException; 8 9 public class IteratorTest { 10 11 public List<String> readFile(String fileName) { 12 List<String> list = new ArrayList<String>(); 13 try { 14 FileReader file_reader = new FileReader(fileName); 15 BufferedReader buffered_reader = new BufferedReader(file_reader); 16 String line; 17 while ((line = buffered_reader.readLine()) != null) { 18 list.add(line); 19 } 20 } catch (FileNotFoundException fnfe) { 21 fnfe.printStackTrace(); 22 } catch (IOException ioe) { 23 ioe.printStackTrace(); 24 } 25 return list; 26 } 27 28 public static void main(String[] argv) { 29 IteratorTest test = new IteratorTest(); 30 List<String> list = test.readFile(argv[0]); 31 Iterator<String> iterator = list.iterator(); 32 while (iterator.hasNext()) { 33 String value = iterator.next(); 34 System.out.println(value); 35 } 36 } 37 }
// リスト 4-7 の実行例 C:\Home> java IteratorTest sample.txt java.util.List java.util.Set java.util.Map java.util.SortedSet java.util.SortedMap java.util.Date java.text.DateFormat org.w3c.dom.Node gnu.regexp.RE jp.ac.wakhok.tomoharu.CSVLine
まず31行目でIteratorを取り出します。32行目でまだ処理をしていないオブジェクトが残っているかどうかチェックし、33行目でオブジェクトを1つ取り出しています。
このようにIteratorを使えば、繰り返しのループ処理を簡潔に書けるのです。
また、Iteratorとよく似た機能を持つ、java.util.Enumerationがあります。JDK1.1まではjava.util.Iteratorはなかったので、VectorやHashtableに含まれているオブジェクトの繰り返し処理に使われます。
J2SE 5.0からは、for文を使ってコレクションを操作することも容易になりました。
次のようなパターンで、コレクションや配列に含まれる要素を繰り返し処理することができます。
for (要素の型 要素名: コレクション or 配列) { …… }
先のIteratorの例は、for文を使って次のようにかけます。Iteratorを使うよりもプログラムが単純になっていることがわかりますね。
for (String value: list) { System.out.println(value); }
コレクション・フレームワークでは、ソートやサーチなどの代表的なアルゴリズムを使ってコレクションを操作できます。このためのクラスとして、
の2つがあります。いずれも、ソートやサーチなどの便利な機能をまとめたものです。java.util.Collectionsはコレクションに対して使い、java.util.Arraysは配列に対して使うものです。
リスト4-7の出力結果を、アルファベット順に並べてみましょう。そのためには、リスト4-7のmainメソッドを、リスト4-8のように変更します。
// リスト 4-8 1 public static void main(String[] argv) { 2 IteratorAndSortTest test = new IteratorAndSortTest(); 3 List<String> list = test.readFile(argv[0]); 4 Collections.sort(list); 5 Iterator<String> iterator = list.iterator(); 6 while (iterator.hasNext()) { 7 String value = iterator.next(); 8 System.out.println(value); 9 } 10 }
// リスト 4-8 の実行例 C:\Home> java IteratorAndSortTest sample.txt gnu.regexp.RE java.text.DateFormat java.util.Date java.util.List java.util.Map java.util.Set java.util.SortedMap java.util.SortedSet jp.ac.wakhok.tomoharu.CSVLine org.w3c.dom.Node
リスト4-7とリスト4-8のmainメソッドの違いは、リスト4-8の4行目の部分だけです。ここでは、List型のオブジェクトlistを「自然な順序付け」に従ってソートします。この場合は、アルファベット順にソートしています。ソートの規則を細かく設定する方法もありますが、ここでは省略します。