例外処理を考える(2)

前回はJavaの例外処理の仕組みについて簡単にまとめました。今回からは実際の例外の扱いについて具体的に考えていきたいと思います。

次の2つの場合を順に考えることにしましょう。

  1. 例外に対処する
  2. 例外を発生させる

例外に対処するときの考え方

何に失敗したのか?

J2SEの標準APIを使っていても、スローされる例外に対処しなければならない機会はそれなりにあるものです。よくあるのはファイル入出力関連・ネットワーク関連・DB関連でしょう。いずれもJVMの外の世界とやりとりするため、予測不可能な事態が発生する可能性は高いと言えます。例外をスローするメソッドが多いのもうなずけます。

「いや、NullPointerExceptionとかIndexOutOfBoundsExceptionをよく見るよ」という方、それはプログラミングエラーです。ご自身のコードをよく見直したほうがいいと思います・・・。

ともあれ、具体的に見てみましょう。ローカルファイルからテキストを読み込んで出力する単純なプログラムを考えてみます。(対処方法はこれから考えていくので、とりあえず例外をキャッチしたらスタックトレースを出力するようにしました)

public void read() {
	File file = new File("sample.txt");
	BufferedReader br = null;
	try {
		br = new BufferedReader(new InputStreamReader(
				new FileInputStream(file), Charset.forName("UTF-8")));		// ---(1)

		String line;
		while ((line = br.readLine()) != null) {		// ---(2)
			System.out.println(line);
		}

	} catch (FileNotFoundException e) {
		e.printStackTrace();
	} catch (IOException e) {
		e.printStackTrace();
	} finally {
		if (br != null) {
			try {
				br.close();		// ---(3)
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
}

あちこちで例外が発生するのでちょっと複雑に見えてしまいますが、「単純な」プログラムです。

このコード中、例外がスローされる箇所は(1)〜(3)ですが、それぞれどんな場合にスローされるのかをJava API 仕様から確認してみると、

(1)FileInputStream(File)は、ファイルが見つからないなど、ファイルをオープンできない場合にFileNotFoundExceptionをスローする。
(2)BufferedReader#readLine()は、何らかの入出力エラーが発生した場合にIOExceptionをスローする。
(3)BufferedReader#close()は、何らかの入出力エラーが発生した場合にIOExceptionをスローする。

となっています。

回復可能か?

例外がスローされた場合に何が起きているのかがわかったら、次はその状態から回復することが可能かどうかを考えます。これもまた順番に考えていきましょう。

(1)で例外が起きた場合、回復は不可能と考えられます。このプログラムでは、ローカルファイル ”sample.txt” が存在して、利用可能であることが前提になっていますね。その前提条件に反した状態から回復しようとすること自体に意味が無いでしょう。
(2)で例外が起きた場合、回復を試みてもよいかもしれません。ファイルからデータを読み込む途中でI/Oエラーが起きることは充分有り得ます。読み込みに失敗したなら、もう一度読み込んでみてもよいでしょう。
(3)で例外が起きることは極めて稀でしょう(単にリソースを解放するだけだから)。仮に例外が発生した場合には重篤なものと考えられます。よって、この状態から回復する必要はありません。

これを反映したコードが以下になります。

public void read() {
	File file = new File("sample.txt");
	LineNumberReader lnr = null;
	try {
		lnr = new LineNumberReader(new InputStreamReader(
				new FileInputStream(file), Charset.forName("UTF-8")));		// ---(1)

		String line;
		for (int i = 0; i < 3; i++) {
			try {
				while ((line = lnr.readLine()) != null) {		// ---(2)
					System.out.println(line);
				}
				break;
			} catch (IOException e) {
				System.err.println(lnr.getLineNumber() + 1
						+ "行目の読み込みに失敗しました。");
				e.printStackTrace();
			}
		}
	} catch (FileNotFoundException e) {
		System.err.println("ファイルのオープンに失敗しました。");
		e.printStackTrace();
	} finally {
		if (lnr != null) {
			try {
				lnr.close();		// ---(3)
			} catch (IOException e) {
				System.err.println("ファイルのクローズに失敗しました。");
				e.printStackTrace();
			}
		}
	}
}

行番号を管理するためにLineNumberReaderを使うことにし、(2)での例外対処については、2回リトライしてみて、ダメならあきらめることにしました。try〜catch節が内側に1つ増えていることに注意してください。

次回へ

このようにして、例外を処理する場合には「何に失敗したのか」「そこからの回復は可能か」を考えていきます。

次回は例外を発生させるときのことを考えてみましょう。

コメントを残す