ファイルデータを読み書きをする処理では、入力ストリーム「in」と出力ストリーム「out」を用意して、データを読み込みながら書き込みをするといったことをしたいときがあります。このとき、finallyで次のように2つのリソースについてcloseするようなことをしがちなので、気を付けましょう。
//略
void exec() throws Exception {
//略
} finally {
in.close();
out.close();
}
}
この場合、せっかくfinallyでinとoutの両方のリソースを解放しようとしていますが、「in.close();」でエラーが発生すると、「out.close();」が実行されないので、out関係のリソースについては解放されずにメモリリークなどの原因となります。
対応方法は「変数へ全部のデータを一時的に保存してから、書き込みをする」とか、「リソースを確保した順と逆順に確実にfinallyでリソースを解放する」ということになります。
前者の場合は次のようなプログラムを書くことになります。「src/sample24/SampleApp.java」の内容をコピーして、「src/sample24/SampleApp.txt」を書き出しています。
package sample24;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
public class SampleApp3 {
public void execute() throws java.io.IOException {
java.util.List lines = new java.util.LinkedList();
BufferedReader in = null;
try {
in = new BufferedReader(new FileReader("src/sample24/SampleApp.java"));
String line = null;
while ((line = in.readLine()) != null) {
lines.add(line);
}
} finally {
if (in != null) {
in.close(); // inはここで確実にclose
}
}
BufferedWriter out = null;
try {
out = new BufferedWriter(new FileWriter("src/sample24/SampleApp.txt"));
for (String line : lines) {
out.write(line);
out.newLine();
}
out.flush();
} finally {
if (out != null) {
out.close(); // outはここで確実にclose
}
}
}
public static void main(String[] args) {
SampleApp3 app = new SampleApp3();
try {
app.execute();
} catch (java.io.IOException e) {
e.printStackTrace(); // 読み込みか書き込みでエラー
}
}
}
データ読み込みに当たっては、BufferedReader型の変数inを用意し、これを使ったらfinally節で確実にcloseしています。データ書き込みに当たっては、BufferedWriter型の変数outを用意し、これを使ったらfinally節で確実にcloseしています。
「リソースを確保した順と逆順に確実にfinallyでリソースを解放する」という場合は、次のようになります。
package sample24;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
public class SampleApp4 {
public void execute() throws java.io.IOException {
BufferedReader in = null;
try {
in = new BufferedReader(new FileReader("src/sample24/SampleApp.java"));
BufferedWriter out = null;
try {
out = new BufferedWriter(new FileWriter("src/sample24/SampleApp.txt"));
String line;
while ((line = in.readLine()) != null) {
out.write(line);
out.newLine();
out.flush();
}
} catch (java.io.IOException e) {
// エラーが発生する場所は複数ある
// new FileWriter()
// new BufferedWriter()
// in.readLine()
// out.write(), out.newLine(), out.flush()
e.printStackTrace();
} finally {
if (out != null) {
out.close(); // outはここで確実にclose
}
}
} catch (java.io.IOException e) {
// new FileReader, new BufferedReader でエラー か
// out.close() でエラー
e.printStackTrace();
} finally {
if (in != null) {
in.close(); // inはここで確実にclose
}
}
}
public static void main(String[] args) {
SampleApp4 app = new SampleApp4();
try {
app.execute();
} catch (java.io.IOException e) {
e.printStackTrace(); // in.close()でエラー
}
}
}
ネストが深くなるため、メソッドに分けて実装することが多いのですが、処理の流れ全体を理解するには一覧性を優先した方がよいので、1つのメソッドにしています。in、out、それぞれについてtryとfinallyを対応させている点、finallyで発生する例外をキャッチできるようにしている点に注目をしてください。
「リソースを確保した順に確実にリソースを解放していく」というのは基本といえば基本なのですが、処理が複雑になってくると、意外と見落としやすくなるため、経験豊かな開発者でもミスをしてしまいます。その結果、リソース解放がされないプログラムを書いてしまうことがあるので、気を付けましょう。
例外を使うと、エラー処理に対して確実にコーディングができるようになり、プログラムの見通しも良くなりますが、「コンパイル時にチェックされる例外」を使用するには下記の3つの注意点も考慮する必要があります。
このように、「コンパイル時にチェックされる例外」であるjava.lang.Exceptionクラスを継承する独自の例外クラスでは強制力があることにより、使いにくい面もあります。従って、場合によってはjava.lang.RuntimeExceptionクラスを継承した独自の例外クラス設計を検討する価値はあります。
ただし、このクラスを使用する場合には、どのメソッドがどんな例外を投げるのかについての情報を完全にドキュメント化しておく必要があります。java.lang.Exceptionクラスのサブクラスであればコンパイラがチェックをしてくれますが、java.lang.RuntimeExceptionクラスのサブクラスについては、そういったチェックがされないので、ドキュメントしか頼りになるものがなくなってしまうからです。
以上のように、例外を使うとエラー処理をうまくコーディングできます。よく理解して使えるようになってください。7つ目は広い意味でのテクニックとして心構えを紹介しておきます。
また、本文では説明をしませんでしたが、メソッドであればエラーコードなども返せますが、コンストラクタではそういった処理をコーディングできません。インスタンス生成時のエラー発生を捕捉するには例外は必須です。
コンストラクタでのエラー捕捉には例外が必須ですが、そうでない場合は、例外はあくまでエラーへ対応するための1つの手段でしかありません。アプリケーション全体を考えた場合は例外を使うよりも、メソッドでエラーコードを返すようにしてエラー処理をした方がいい場合もあります。どういった方法がいいのかを常に考えながらコーディングするようにしましょう。
また、例外を使うと処理の流れを変えられるので、条件文のような使い方も可能です。しかし、例外はその名のとおり、「例外的な処理が発生したことを通知する、その通知を捕捉する」という目的で設計されています。処理の流れを変えるために例外を使うのは何のメリットもないので、例外処理本来の目的以外で使用しないようにしましょう。
複数のリソースを取り扱うときには、リソースの解放をfinally節で確実に実施するように意識すると、メモリリークの発生しないプログラムが作れるようになるはずです。有効に利用しましょう。ちなみに、今年正式リリースされたJava SE 7では、リソースに対してtry文の対応付けが可能になったので、簡単になっています。興味のある読者は調べてみてください。
こういった点に注意しながら、例外処理の仕組みをよく理解したうえで有効に利用できるようになってください。 今回作ったサンプルのソースコードは、こちらからダウンロードできます。
小山博史(こやま ひろし)
情報家電、コンピュータと教育の研究に従事する傍ら、オープンソースソフトウェア、Java技術の普及のための活動を行っている。長野県の地域コミュニティである、SSS(G)やbugs(J)の活動へも参加している。
著書に「基礎Java」(インプレス)、共著に「Javaコレクションフレームワーク」(ソフトバンククリエイティブ)、そのほかに雑誌執筆多数。
Copyright © ITmedia, Inc. All Rights Reserved.