ファイルI/O

I/O(Input/Output)は情報処理では不可欠の概念で,様々なデバイスからデータを読み込んだり,
デバイスに対してデータを送ったりすることです.
ここでは特に,ディスクをはじめとする補助記憶装置に対するI/Oについて説明します.

導入   ファイルからの入力   エラーに対する処理(例外処理)   ファイルへの出力


■ 導入

ここでは,ファイルからデータを読み取ったり,データをファイルとして保存する方法を説明します.

ディスク上ではファイルは名前をつけて保存されています.ファイルに対する入出力は,
(1)対象のファイルのファイル名を指定して入出力の準備を整える作業に始まり,
(2)実際の入出力を行い,入出力の処理を終えた後は(3)ファイルの使用の終了を宣言する
流れとなります.

ディスク上に保存されるファイルも,その正体は「1バイト毎のデータが1直線に並んだ列」なので,
基本的には,
 ● ファイルから1バイトづつデータを読み込む(入力)
 ● ファイルに1バイトづつデータを書き込む(出力)
という操作が基本になります.

このように1バイトづつデータが流れる様子を「ストリーム」と表現し,
 「入力ストリームから1バイトづつデータを読み取る」 とか
 「出力ストリームへ1バイトづつデータを送る」 といった表現をします.

実用的なプログラミングにおいては,1バイトづつの入出力だけでなく,1行単位でまとめて
読み込んだり,書き出したりしますし,一般的なバイナリデータは「〜バイトまとめて」入出力したり
することもありますが,順番に学んで行くことにしましょう.


ページトップへ↑

■ ファイルからの入力

ディスクに保存されているテキストファイルからデータを読み込む処理について説明します.

【1バイト単位の入力】

ファイルからデータを1バイトづつ読み取る処理を例にあげて,バイト単位での入力の方法を説明します.

まず,次のようなサンプルデータ "test1.txt" を準備します.

データファイル:test1.txt
Hello, everyone. I am now studying a programming.
This is a test data for file input practice and consists of 3 lines.
Let's enjoy programming together.

このファイルからデータを1バイトづつ取得して,それを画面に印字出力する処理を考えます.

「基本」のページでは "System.in" というものに対して "read()" をいうメソッドを作用させて
データを1バイト取り出す処理について説明しましたが,今回の場合も考え方は基本的に同じです.

上の "test1.txt" というファイルを指し示すオブジェクトに対して "read()" というメソッドを
作用させることで,そのファイルからデータを1バイト取得することができます.

また,ファイルの内容全体を読み取って画面に印字するには,読み出しの処理をファイルの内容の先頭から
末尾に至るまで繰り返す必要もあります.つまり繰り返しの制御も必要になるわけです.

実際に処理を行うプログラムの例 "Filein1.java" を次に示します.

プログラム:Filein1.java
import java.io.*;

class Filein1 {
	public static void main( String argv[] ) throws java.io.IOException {
		FileInputStream fi;
		int b;
		//--- selection of input file ---
		fi = new FileInputStream("test1.txt");
		//--- input loop ---
		while ( (b = fi.read()) != -1 ) {
			System.out.printf("%c",b);
		}
		fi.close();
	}
}

Javaでは,あらゆる対象を「オブジェクト」という概念の実体として扱います.今回はファイルを
入力ストリームと見てそこから1バイトづつデータを取り出しますが,この「入力ストリーム」を
表すオブジェクトがプログラムの5行目で宣言されている "FileInputStream" というクラスの
"fi" というオブジェクトです.

8行目に "fi = new FileInputStream("test1.txt");" という記述がありますが,
「"test1.txt"というファイルを指し示すオブジェクトを新たに生成して,それを "fi" とする」
という処理をしています.

言ってみれば
「"fi" にファイル "test1.txt" が連結され,
   以後はfiをそのファイルだとみなして入力処理を行えば良い」
ということになります.

10行目から12行目は while 文による繰り返し処理になっている.この繰り返しは,
「1バイトづつ読み込んではそれを印字する処理」
をファイルの先頭から末尾にかけて繰り返すものである.

ファイルの全ての内容を読み終われば処理を終了するが,13行目はfiの使用の終了を宣言するものである.
「fiに対してcloseを実行する」ことを意味する.
("close" メソッドを実行することでファイルの使用終了となります)


ページトップへ↑

■ エラーに対する処理(例外処理)

さて,上のプログラム "Filein1.java" 4行目に "throws java.io.IOException" という記述が
ありますが,これは「例外処理」を設定するためのもので,言ってみれば,プログラム実行時にエラーが
発生したらどうするかを設定するためのものです.

実際のプログラムの実行環境では,実行中にエラーが発生することがあり,実行を継続できなくなる
ケースがあります.例えば,入力用にファイルをオープンしようとしたとき,そのファイルが存在しない
場合はエラーとなります.今回のプログラムでも,もしも入力ファイル "test1.txt" が存在しない場合,
プログラムを実行すると次のようなエラーメッセージが表示されます.

エラーが発生した場合に対する処理が「例外処理」であり,4行目のような記述をすることで上のような
メッセージを出力するという例外処理が発動したわけです.

エラー発生時の例外処理を独自に作成することも可能で,それを実現しているのが次のプログラム
"Filein2.java" です.

プログラム:Filein2.java
import java.io.*;

class Filein2 {
	public static void main( String argv[] ) {
		FileInputStream fi;
		int b;
		//--- selection of input file ---
		try {
			fi = new FileInputStream("test1.txt");
			//--- input loop ---
			while ( (b = fi.read()) != -1 ) {
				System.out.printf("%c",b);
			}
			fi.close();
		}
		//--- error handling (original) ---
		catch(IOException e) {
			System.out.println("ファイル入力でエラーが発生しました.");
		} finally {
			System.out.println("プログラムを終了します.");
			System.exit(1);
		}
	}
}

プログラムの中で,エラーが発生する可能性のある部分を "try" 文の中に記述すると,
エラー発生時に "catch" 文の中に記述したプログラムが強制的に実行されます.

このプログラムでは,ファイルを開いて処理を行いファイルを閉じるまでの一連の処理を
try の中に記述しています.これにより,ファイル関係のエラー(IOException)が発生すると,
catch 文の中に記述したプログラムにより "ファイル入力でエラーが発生しました." が表示
されます.(下図参照)

次のように,catch 文はエラーの種類毎に複数書き連ねることができます.
また,すべての catch 文の終了後に共通に行う処理を finally 文の中に記述します.

エラー発生時の例外処理の記述  try {
     エラーの可能性のある処理
 } catch( エラーの種類1(例外の型1) 引数 ) {
     例外処理1
 } catch( エラーの種類2(例外の型2) 引数 ) {
     例外処理2
 } catch( …
     :
 } finally {
     共通の処理
 }

"Filein2.java" では,try,catch の実行の後,finally 文の中で
  "プログラムを終了します."
と表示して "System.exit(1)"(強制終了メソッド)によりプログラムを終了します.



【行単位の入力】

テキストデータは行単位で構成される文書情報のための基本的な形式です.
テキストデータを行単位で読み込んで文字列型の変数に格納する方法について説明します.

先に説明した "Filein1.java","Filein2.java" では,ファイルを「入力ストリーム」
(FileInputStreamクラスの実体)とみなして1バイトづつデータを読み込みました.

今回は行単位の読み込みがテーマで,この場合はファイルを「リーダ」という種類の実体と見て
読み込みを行います.
(1バイトづつではなく,まとめて読み込む対象を「リーダ」というのだと理解してください)

具体的には,テキストを「行単位」とかある程度まとまったデータの塊として1度に読み込む場合は,
入力を "BufferedReader" というオブジェクトとして扱います.

次のプログラム "Textin.java" を見てください.

プログラム:Textin.java
import java.io.*;

class Textin {
	public static void main( String argv[] ) throws java.io.IOException {
		BufferedReader fi;
		String s;
		int n = 0;
		fi = new BufferedReader( new FileReader("test2.txt") );
		while ( (s = fi.readLine()) != null ) {
			n++;
			System.out.println(s);
		}
		fi.close();
		System.out.println("----以上"+n+"行----");
	}
}

プログラムの8行目に "fi = new BufferedReader( new FileReader("test2.txt") );"
という文があります.ちょっと複雑な感じがしますが,

「"test2.txt" というファイルを示す "FileReader" というクラスのリーダを生成し,それを元にして
 テキストをまとめて読み込むための "BufferedReader" というクラスのオブジェクトを生成する」

という処理をしています.この結果 "fi" というテキストストリームができます.

「テキストストリーム」というのはバイトストリームと違い,「行単位」のようにテキストデータを
まとめて読み込むためのものだと考えてください.

あとはこのfi に対して "readLine" メソッドを実行すると,そのファイルから1行読み込むことが
できます.もしもファイルの読み込みが終了している("EOF"の状態)場合,readLine メソッドは "null"
を返します.プログラム9行目にある
   "(s = fi.readLine()) != null"
は,readLine メソッドの結果がnull でないことを判定して繰り返しを実行するための条件です.

ではこのプログラムを実行してみましょう.入力データとして次のような内容のファイル "test2.txt"
を用意します.

データファイル:test2.txt
(はじまり)
テキストデータを行単位で読み込むプログラムのための
テストデータです.

このデータは日本語で書かれています.正しく表示されていますか?
(おわり)

このファイルを用意して,上のプログラム "Textin.java" をコンパイルして実行すると次のような
結果になります.

 


【コマンド画面からの行単位入力】

BufferedReader はコマンド画面からの入力(標準入力)に対しても使用することができます.
先の例で,BufferedReader にファイル FileReader("test2.txt") を割り当てましたが,これの替りに
InputStreamReader(System.in) を割り当てると,コマンド画面入力(標準入力)をテキストストリーム
として扱うことができます.

標準入力から行単位で入力するプログラム "Stdin2.java" について考えましょう.

プログラム:Stdin2.java
import java.io.*;

class Stdin2 {
	public static void main( String argv[] ) throws java.io.IOException {
		BufferedReader fi;
		String s;
		int n = 0;
		fi = new BufferedReader( new InputStreamReader(System.in) );
		while ( true ) {
			n++;
			System.out.print(n+": 入力> ");
			s = fi.readLine();
			if ( s != null ) {
				System.out.println(n+": 出力>「"+s+"」が入力されました.");
			} else {
				break;
			}
		}
		fi.close();
		System.out.println("----以上"+n+"行----");
	}
}

これは,標準入力から文字列を1行読み取ってそれを文字列変数に格納し,それをそのまま標準出力
に出力するプログラムです.基本的な考え方は,先のプログラムTextin.java とほぼ同じです.

ただし,テキストストリームに標準入力を割り当てるため,8行目のような記述になります.

今回のプログラムでは,while 文の繰り返し条件として‘true’(真)を指定し,常に繰り返しを実行す
る形にしています.入力処理は12行目で行い,入力が終了(キーボード入力の場合は ctrl + Z
押されたとき)すると "break" で while による繰り返しを強制終了します.

繰り返し制御の強制終了 break文を実行することで,while や for による繰り返し制御を強制終了することができる.

このプログラムをコンパイルして実行した結果を下に示します.


【型を指定した読み込み】

Scanner クラスを用いると,数値など指定した型のデータをファイルから順番に読み取ることができます.
先に用いた BufferedReader の代わりに Scanner クラスのオブジェクトを生成し,それに対して読み込みの
ためのメソッドを呼び出すことでそれが可能になります.

下記のプログラム "Filein3.java" は,ファイル "test3.txt" から整数を10個読み込み,その合計を
求めるものです.

プログラム:Filein3.java
import java.io.*;
import java.util.Scanner;

class Filein3 {
	public static void main( String argv[] ) throws java.io.IOException {
		Scanner si;
		int a, c, n = 0;
		si = new Scanner( new FileReader("test3.txt") );
		for ( c = 0; c < 10; c++ ) {
			a = si.nextInt();
			n += a;
		}
		si.close();
		System.out.println("合計="+n);
	}
}
データファイル:test3.txt
1
2
3
4
5
6
7
8
9
10

Scanner クラスを使用するには,"java.util.Scanner" をインポートする必要があり,2 行目で
それを行なっています.8行目でファイル "test3.txt" を入力元とする Scanner クラスのオブジェクト
si を生成しています.

このプログラムをコンパイルして実行した例を下に示します.

Scanner オブジェクトから値を読み込むメソッドには次のようなものがあります.

メソッド働き
nextInt整数値(int)の読み込み
nextLong整数値(long)の読み込み
nextDouble浮動小数点数(double)の読み込み
nextByteバイト値(byte)の読み込み
nextBoolean真理値(boolean)の読み込み

今回のプログラムは,si に対してnextInt メソッドを実行して整数値を読み込むものです.

ページトップへ↑


■ ファイルへの出力

ファイルにデータを保存する方法について説明します.


【1バイト単位の出力】

データを1バイトづつ出力する方法について説明します.write メソッドを使用する形で先の
プログラム "Filein1.java" を改造して,データをコピーする機能を実現するプログラム "Fcopy.java"
をつくります.

(考え方)
入力ストリームから1バイトデータを読み込んで,それを出力ストリームに出力するという処理を繰り返す.

プログラム:Fcopy.java
import java.io.*;

class Fcopy {
	public static void main( String argv[] ) throws java.io.IOException {
		FileInputStream fi;
		FileOutputStream fo;
		int b;
		long n = 0;
		//--- selection of files ---
		fi = new FileInputStream("test1.txt");
		fo = new FileOutputStream("clone.txt");
		//--- input loop ---
		while ( (b = fi.read()) != -1 ) {
			n++;
			fo.write(b);
		}
		fo.close();
		fi.close();
		System.out.printf("%dバイトを複写しました.\n",n);
	}
}

プログラムの6行目で出力ストリーム FileOutputStream クラスのオブジェクトとして "fo" を宣言
しています.これに対して実際の出力用のファイル "clone.txt" を実際に割り当てているのが11行目です.

読み込んだバイトデータは変数bに格納され,これをwrite メソッド(15 行目)でストリーム fo に出力
しています.


【行単位の出力】

これまでコマンド画面(標準出力)に出力する方法として print, println, printf といった
メソッドを用いてきましたが,これらメソッドは出力ファイルに対しても使用することができます.

つまり「ファイルに対して印字する」という考え方で出力するわけです.

出力のためのテキストストリームとして PrintWriter があります.これを利用したプログラム
"Textout.java" を見てみましょう.

プログラム:Textout.java
import java.io.*;

class Textout {
	public static void main( String argv[] ) throws java.io.IOException {
		PrintWriter fo;
		fo = new PrintWriter( new FileWriter("output.txt") );
		//--- 改行せずに続けて出力 ---
		fo.print("あ");
		fo.print("い");
		fo.print("う");
		fo.print("え");
		//--- 行末で改行する出力 ---
		fo.println("お");
		fo.println(1+"a"+2+"b"+3+"c");
		//--- 書式付き出力 ---
		fo.printf("書式付き出力:%5.2f,%s,%d",3.141592,"文字列データ",8765);
		//--- ファイルを閉じる ---
		fo.close();
	}
}

5行目で出力用のテキストストリーム fo が宣言されており,それに対して6行目で実際のファイル
"output.txt" が割り当てられています."FileWriter('output.txt')" は "output.txt" を
出力ファイルとして開くことを意味します.後は,fo に対して print,println,printf メソッドを
実行して「ファイルに印字」すればいいわけです.

このプログラムを実行した結果,ファイル "output.txt" の中に出力結果(下記)が書き込まれます.

出力ファイル:output.txt
あいうえお
1a2b3c
書式付き出力: 3.14,文字列データ,8765

ページトップへ↑