最終回の主なテーマは、 ファイル、キーボードや画面への入出力です。 今までのプログラムは、 データをプログラムの中に決めうちで埋め込んでしまい、 計算結果を画面に表示するものでした。 ファイル名を指定し、ファイルからデータを読み込んだり、 データを書き込んだりする方法や、 キーボードからの入力を読み込む方法を説明します。
ファイルの入出力を Java のプログラムで行うためには、 いくつかの約束ごとをマスターする必要があります。 ここでは、 例を用いてファイルの入力を行うプログラムの書き方を説明します。 対象はテキストファイルです。
ファイルへの入出力は、 Java のクラスライブラリにあらかじめ用意されている、 いくつかのクラスを利用することにより行います。
import java.io.*; class DisplayFile { public static void main(String args[]) { BufferedReader in = null; try { in = new BufferedReader(new FileReader("sample.txt")); } catch (IOException e) { System.err.println("File cannot be opened."); System.exit(1); // ←終了コード 1 でプログラムを終了するメソッド } try { String str; str = in.readLine(); while (str != null) { System.out.println(str); str = in.readLine(); } } catch (IOException e) { System.err.println("An error was occured while reading."); } try { in.close(); } catch (IOException e) { System.err.println("An error was occured while closing."); } } }
このプログラムは、指定したテキストファイル (この例では sample.txt) の内容をそのまま画面に出力するものです。
新しいキーワード import と try 〜 catch があります。 これについて詳しくは後程説明します。 ここでは、プログラムがすべて順調に実行されると、 try の中カッコの中のみが実行されるとだけ考えてください。
ファイルからの入力の手順は次のとおりです。
まず、最初にファイルをオープンします。 オープンの際には、ファイル名を指定して、 ファイルの読み書きを行うことをシステムに準備させます。 システムは、その名前のファイルが存在するかどうかを調べるなど 必要な作業を行い、 ファイルの入出力を行う機能を持つオブジェクトを生成します。
例題のプログラムは次のようになっています。
in = new BufferedReader(new FileReader("sample.txt"));
new 演算子を用いてインスタンスを二重に生成しています。 これは、次のように書いてもほぼ同じ動きをします。
innerReader = new FileReader("sample.txt"); in = new BufferdReader(innerReader);
ここで得られた in は、 ファイル sample.txt の読みこみを行う機能を持ったオブジェクトです。 これ以降ファイルに対する読みこみ操作の際には、 in のメソッドを呼び出します。
ファイルがオープンされると、 実際にファイルからデータを読みこむことができるようになります。
次に、
str = in.readLine();
では、メソッド readLine を用い、 sample.txt から実際に 1 行ぶん読みこみ、変数 str に代入しています。 メソッド readLine を次々呼び出すことで、 ファイルの内容を先頭から順に読みこむことができます。 ファイルの終りに達すると readLine は null という値を返します。
ファイルに対するすべての読みこみが終わったら、 最後に close メソッドを実行します。 close によって in と実際のファイル (sample.txt) との結び付きが解除されます。
Java では、ファイル、キーボードや画面との入出力など、 プログラムの外部からデータが出入りするものをストリーム (stream) と呼ぶ概念で統一して扱います。
実際にデータの入出力を行う際には、 対象とするデータの種類やデータのある場所によって、 行うべき処理内容が異なってきます。 例えば、ローカルのディスクのファイルに対する入出力と、 キーボード/画面に対する入出力と、 ネットワークを介したサーバのデータ転送では、処理内容が異なります。
Java ではオブジェクト指向の特徴を生かし、 入出力データ全般に成り立つ性質をストリームと呼ばれるクラス群に持たせ、 それを継承する形で様々な種類のデータに対応するよう、 多くのクラスが用意されています。
先の例題の FileReader はファイルを読みこみ可能な状態にし、 実際にファイルから 1 文字ずつ読みこみを行う機能を持つストリームです。 FileReader は、 1 文字ずつファイルの内容を読み込むことしかできないので不便です。 そこで、行単位でファイルの読み込みを行う BufferedReader を用い、 FileReader のインスタンスにかぶせて使うことで 行単位の処理ができるようになります。
この「かぶせる」ことを英語で wrap と書くことから、 ラップすると呼ぶこともあります。 これは、以前説明した包含の考え方です。
ファイルへの出力を行う場合、 クラス FileWriter, BufferedWriter, PrintWriter の 3 つのクラスを用います。
import java.io.*; class WriteString { public static void main(String[] args) { PrintWriter out = null; try { out = new PrintWriter(new BufferedWriter( new FileWriter("sample.txt"))); } catch (IOException e) { System.err.println("File cannot be opened."); System.exit(1); } out.println("Hello, Java."); out.println("You not only study Java,"); out.println("but you also have to study data structures and algorithms"); out.close(); } }
まず、
out = new PrintWriter(new BufferedWriter( new FileWriter("sample.txt")));
については、次のように説明できます。
クラス FileWriter は 1 文字単位でファイルへ出力を行う基本的な機能を持つストリームです。 これを BufferedWriter によって行単位で出力を行えるように拡張 (ラップ) します。 さらに、 PrintWriter によって、 様々な型のデータをメソッド println を用いて出力することができるように 拡張します。
クラス PrintWriter のインスタンス out に対しては、 メソッド println や print を用いて データを出力することができます。 これらは、 今まで画面表示のために使ってきたメソッド System.out.println や System.out.print と同じものです。
特にファイルへの書き込みを行う場合、 メソッド close でのファイルのクローズは忘れてはなりません。 必ずクローズしてください。 println や print の呼び出しによって書き込まれるデータは、 いったんバッファ (buffer) と呼ばれる一時的な記憶領域に保存され、 別のタイミングで実際にファイルに書き込まれます。 ファイルをクローズすることで、 バッファに残っているまだ書き込まれていないデータを 最後までファイルに書き込む処理が行われます。 クローズを忘れるとファイルの内容が中途半端になる可能性があります。
実は、これまで使ってきた System.out は、 システムに標準で備わっている画面に出力するためのストリームなのです。 System.out のストリームは特にインスタンスを生成しなくても、 使用することができるのです。
キーボードから入力を行うためのストリームに System.in があります。 次のプログラムは、 キーボードから入力された内容をそのまま画面に表示するプログラムです。
import java.io.*; class KeyboardInput { public static void main(String[] args) { BufferedReader keyboard = new BufferedReader( new InputStreamReader(System.in)); try { String str; str = keyboard.readLine(); while (str != null) { System.out.println("Input Data: " + str); str = keyboard.readLine(); } } catch (IOException e) { System.err.println("An error occured in key input"); } } }
このプログラムでは、 まず、キーボードからの入力ストリーム System.in を クラス InputStreamReader, BufferedReader で拡張 (ラップ) した インスタンスを生成しています。
次に、ここで生成したオブジェクト keyboard に対して、 メソッド readLine を用いることで行単位、 すなわち、 Enter または Return キーが押されるまでの入力を受け取り、 str に代入しています。 キーボードからの入力が終了する場合は、 Control+D (Control キーを押しながらアルファベットの D のキー) を押します。 プログラムの中では、入力は終了したものとして readLine の値が null となります。
System.out, System.in を用いて入出力を行うことを行ってきましたが、 このときの入出力の相手を標準出力および標準入力と呼びます。 特に指定がなければ、標準出力はディスプレイ画面、 標準入力はキーボードに結び付けられます。
Linux をはじめとする UNIX では、プログラムを起動する際に、 標準入力や標準出力を特定のファイルに結び付けるように指定することができます。 このようなファイルの切り替えをリダイレクトと呼びます。
例えば、
$ java AClass > ファイル名
とすると、プログラムの出力が指定されたファイルに書き込まれます。 普通なら画面に出力されていた内容が出なくなり、 同じ内容がファイルに記録されます。 これは、プログラムの出力結果をファイルに残したい場合などに便利です。 リダイレクトは Java に限らず UNIX のコマンドであれば、ほとんどのものに同じように適用できます。
逆に、
$ java AClass < ファイル名
とすると、キーボードの代わりに入力データをファイルから読み込みます。 プログラムに処理させたいデータが大量にある場合、 キーボードからの入力は大変なので、 あらかじめエディタや別のプログラムでデータを用意してから、 この方法で入力させることができます。
さらに、
$ java AClass < 入力ファイル名 > 出力ファイル名
のように、入力と出力の両方を別々のファイルに結び付けることもできます。
ところで、もう一つ標準エラー出力と呼ばれるものがあります。 標準エラー出力は、もっぱらエラーメッセージを出力するために使われます。 標準エラー出力のストリームは System.err です。 例題のプログラムでは、エラーメッセージの出力に System.out ではなく System.err を使っています。
特に指定がなければ、標準エラー出力は画面上に出力されます。 標準出力と別のものを用意してあるのは、 リダイレクトを行った場合でもエラーメッセージは画面に表示させたいからです。 上の例のように標準出力をリダイレクトしていても、 標準エラー出力は画面上に表示されるので、 エラーメッセージを見逃してしまうことはありません。
これまで説明したストリームの使用方法は、 主にテキストファイルを対象とした方法です。 画像などのバイナリデータを取り扱うにはクラス InputStream/OutputStream を用いる必要があります。 詳細は本講義では取り上げません。 エッセンシャル Java pp.287-302 を参照してください。
プログラムの実行中に、 望まれた動作ができない状況が発生することがあります。 例えば、ファイルを読みこもうをしたときに指定されたファイルが存在しない場合や、 ファイルが書き込み禁止になっていたりした場合です。 このような、プログラムの実行中に起きる問題を例外 (exception) と呼びます。
ファイルが正しく書き込めなかったのを無視して作業を進めれば、 せっかくの処理結果を失ってしまい、これでは大変なことになります。 例外が発生したとき、 その問題を正しく処理する必要があります。
ファイルのオープンや、読み込み、書き込みなど、 例外が発生する可能性のある処理を try の中カッコの中に記述します。 try の中の処理で例外が発生した場合、 catch の中の処理が行われます。 ここには、エラーメッセージを表示したり、 別の処理で代替するなどの、 例外の原因となった問題を解決するための処理を書きます。
try 〜 catch 文の文法的な説明は以下のとおりです。
try { .... 例外が発生する可能性のある処理 .... } catch (例外の型 変数名) { 「例外の型」と同じ例外が発生した際のエラー処理 .... } catch (例外の型 変数名) { 「例外の型」と同じ例外が発生した際のエラー処理 .... } ... ... finally { 例外の発生の有無に関わらず行われる処理 .... }
実は、例外もクラスとして表されます。 例外の種類によって、さまざまな例外のクラスがあります。 例えば、ストリームを用いて入出力を行っている際に よく発生する例外は IOException です。
例えば、例外 IOException が発生したときの例外処理は次のようになります。
try { ... ... } catch (IOException e) { 例外処理の内容 ... }
変数 e は例外のオブジェクトです。 例外のオブジェクトは、 例外の起こった場所、直接の原因など、 例外処理に役立つ様々な情報を持っています。
例外処理についての詳細は、エッセンシャル Java pp.197-212 を参照してください。
Java のクラスライブラリには様々なクラスが用意されていますが、 これらは階層的に管理されています。 例えば、 FileReader は正確には java.io.FileReader というクラスです。 これは、java.io というパッケージに属する FileReader というクラス、 という意味です。
クラス java.io.FileReader を使用するには、単に
in = new java.io.FileReader("sample.txt");
のようにクラス名の正確に書けば良いのですが、クラス名が長くなり面倒です。 プログラムの冒頭で import 文を書くことにより java.io を省略することができます。
import java.io.FileReader; ... ... in = new FileReader("sample.txt"); ...
さらに、 import java.io.*; のようにアステリスク (「*」) を用いると、 パッケージ java.io に属するすべてのクラスが import されます。
ただし、 java.lang パッケージに属するクラスは、 import 文を使用しなくて使用できます。 java.lang に属するクラスの例には、 String や System などがあります。
パッケージについての詳細は、 エッセンシャル Java pp.193-196 を参照してください。
Java のクラスライブラリの パッケージとクラスの構成については、 JavaTM 2 Platform, Standard Edition, 1.4.0 API 仕様、 もしくは、 /usr/java 以下に展開した同様のドキュメントを参照してください。