9-2: 入出力の実現


・ ストリームのクラスを利用して入出力を行う サンプルプログラム CpCommand.java を紹介します。 コマンドラインの第1引数に指定された名前のファイルの内容を、 コマンドラインの第2引数に名前を指定されたファイルにコピーします。 細かなオプションの指定などの機能はありませんが、 UNIXの cp や MS-DOSの copy コマンドに相当するプログラムです。
 ファイルからの入力を行うために FileInputStreamのオブジェクトが、 ファイルへの出力を行うために FileOutputStream のオブジェクトが生成されます。 ここでは、それぞれのオブジェクトのコンストラクタに、 ファイルのパス名として解釈されるコマンドライン引数 (Stringのオブジェクト)が渡されています。 したがって、実行する際にはたとえば次のようにしてください。
 入出力は交互に読み書きを繰り返すループによって処理されます。 入力を行うのは FileInputStreamの read()メソッド、 出力を行うのは FileOutputStreamの write()メソッドです。 これらの2つのメソッドは、 他のすべてのストリームのクラスに共通する最も基本の存在です。 いくつかのストリームのクラスでは、 目的に応じて機能を追加された入出力メソッドが利用されることもありますが、 それらも read()もしくは write()を元に記述されています。 (C言語でシステムコールのプログラミングがある人なら、 read(),write()が低水準入出力のシステムコールを意識して 付けられた名前であることに気づいたことでしょう。)
 read(),write()は1バイト単位で行われていますが、 byte型の配列を引数に渡して複数のバイトを入出力することも可能です。 また FileReader や FileWriter を用いなかったのは、 バイナリファイルのコピーもできるようにするためです。 上のサンプルは1バイトずつコピーするだけですから、 もちろんテキストファイルのコピーも問題なく行うことができます。 ファイルのコピー元として Javaのソースファイルの場合とバイトコードの場合の 両方を試してみれば、そのことが確認できるでしょう。
 上のサンプルを紹介した主な目的は、 ストリームによる入出力のプログラムの基本部分を確認してもらうことです。 この基本部分は他のストリームのクラスの場合でも変わりません。 行うべきことは次の2点のみです。

  1. 目的に応じたストリームのオブジェクトの生成
  2. ストリームのオブジェクトの read()および write()メソッドの呼び出し

 さて次に進む前に、 例外処理の記述について少し解説しておきましょう。 Javaにはエラー処理に特化した制御構造として「例外処理」の機構が存在します。 try は例外を検出するためのキーワードです。 try に続くブロック内には、例外が発生する可能性がある処理を記述します。 処理の中で例外が発生すると、 tryブロックの後に続く catchブロックのうちの適当な処理が呼び出されます。 どの catchブロックが選択されるかは、 発生した例外のオブジェクトのクラスによって決まります。 つまり try と catch はブロック単位の処理のうちのいずれかが選択されるという 点では if と else の関係に似ていますが、 場合分けのスタイルはむしろ switch と case の関係に似ていると言えるでしょう。
 コマンドラインの引数が足りなくて変数 argv[0], argv[1] にデータが 与えられていない場合には ArrayIndexOutOfBoundsException が発生します。 それが検出された場合は正しい使用方法をエラーメッセージとして表示します。 コマンドライン引数として文字列が与えられても、 それが不適切な場合もあるでしょう。 FileInputStream のコンストラクタは、 与えられたパス名のファイルが存在しない場合に FileNotFoundException を発生します。 それが検出された場合には、 コピー元のファイルが存在しないことをエラーメッセージとして表示します。 入出力の処理中に何らかの理由で障害が発生したような場合には、 read()および write()メソッドが IOException を発生します。 それが検出された場合には、 もっと一般的なエラーと区別してエラーメッセージを出すようにします。 このように例外処理を導入することで、 従来のエラー処理の記述に比べてわかりやすい形式にまとめることが可能になります。
 なお、複数の catchブロックを並べる場合に注意すべきことが1つあります。 それば catch の場合分けの順番にちゃんと意味があるということです。 より特殊な例外のクラスを上に、より一般的な例外のクラスは下に記述します。 上のサンプルの場合でも、FileNotFoundException は IOException のサブクラス ですから、必ず IOException より上に来ます。 互いに継承関係がない場合にはどちらが先に来てもかまいません。 Exception の処理は一番最後に来ます。 なぜなら通常のアプリケーションで用いられる例外のクラスは、 すべて Exceptionのサブクラスだからです。 このような順番のルールが守られていないとコンパイル時にエラーとなります。

・ 今度はファイルから読み込んだ内容を標準出力に表示する サンプルプログラム CatCommand.java を紹介しましょう。 UNIXで言えば cat 、MS-DOSなら type コマンドに相当する機能を提供します。
 このプログラムが標準出力に表示する対象はテキストファイルです。 したがって読み込みには FileReaderを利用してみましょう。 出力先は標準出力ですから、Systemクラスが提供する PrintStreamのオブジェクト out を使用します。 PrintStreamは write()よりも高レベルの機能を備えた print()メソッドを提供します。 ここでは char型のデータを出力する目的で write() の代わりに用います。 その他の点は例外処理の部分を含め、先の基本のサンプルとほとんど同じです。
 また1文字読んで1文字出力という処理はあまり効率の良い方法とは言えません。 この点を改善する方法も導入しています。 先にストリームのクラス群を紹介した中に BufferedReader のように頭に Buffered が付くクラスが、4つのグループのいずれにも存在します。 これらのクラスはその名前のとおり、 バッファリングの機能を備えたストリームを提供してくれます。 ここでは BufferedReaderが用いられています。
 BufferedReaderは具体的な入出力の対象に対応するストリームではありません。 既存のストリームにバッファリングの機能を持たせるためのクラスです。 したがって、たとえば BufferedReaderの生成には他の Readerのオブジェクトをコンストラクタの 引数として与える必要があります。 上のサンプルでは FileReader のオブジェクトを先に生成し、 2段階の生成を経て BufferedReaderのオブジェクトを得ます。 いったん BufferedReaderのオブジェクトが得られれば、 データは自動的に適切なサイズのバッファを通じて入力されるようになります。 プログラムの中で BufferedReaderからの入力をどう記述しても、 現実のバッファリングには影響を与えません。 サンプルでは行単位の入力の形式で記述されています。 そのために使用されているのが readLine()メソッドで、 BufferedReaderからの入力処理では最も頻繁に用いられます。
 ところで BufferedReaderの使用するバッファのサイズはどこで決められるので しょうか? BufferedReaderを生成する時にバッファサイズを明示的に指定することも可能です。 しかし一般にその方法は好ましいやり方ではありません。 なぜかと言うと、Javaのアプリケーションは実行される時によって システムやハードウェアが違っているかもしれないからです。 最適のバッファサイズが常に同じではないわけです。 したがってプログラム内に明示的にバッファサイズを埋め込むことは避け、 仮想マシンがシステムに合わせて提供してくれるデフォルトのバッファサイズを 使用する方が実は賢明なわけです。