1. 無料アクセス解析
PGKiss例外処理(try,catch,finally)を記述する基本原則 2008/4/13

例外処理(try,catch,finally)を記述する基本原則 - Javaとかで学ぶ、すぐに使いたくなる洗練されたプログラムイディオム集

例外処理(try,catch,finally)は、if文やfor文などと比べ、歴史の浅い制御文だ。歴史が浅い分、例外処理の書き方も確立・定着しておらず、人によってまちまちだ。そこで今回は例外処理の書き方を取り上げる。

例外処理をまじめに実装するとかなり複雑なことになる。そのため今回はシンプルにするイディオムというよりも、シンプルな基本原則を示すことにする。

例外処理の基本原則

1. 後処理にはfinallyを使う

リソースの開放などの必ず実行したい後処理は、必ずfinallyブロックの中に書く。その際のtryブロックは後処理が必要になる事由の発生直後から始める。

例:
1 Connection con = newConnection();
2 try {
3     // do something
4 } finally {
5     con.close();
6 }
7 
2. 例外の識別が必要な場合以外は例外をcatchしない

catchは一種の分岐なので、安易なcatchの利用はプログラムを必要以上に複雑にする。例外の種類によって処理を分ける必要がある場合(Javaの場合検査例外(ExceptionのサブクラスかつRuntimeExceptionのサブクラスでない例外)をRuntimeExceptionでラップする場合も)を除いて、原則catchを利用しないようにする。例外発生のログを出力するのは(呼び出し関係の)上位(mainメソッドとか)で一括して行うとか、後処理をするためのcatchは上記のようにfinallyを使うなどで、多くのcatchを取り除くことができる。

3. 後処理に複数の処理が必要ならtry-finallyで連ねる

次のコードでは、cleanup1で例外が発生すると、cleanup2は実行されない。

1 try {
2     // do something
3 } finally {
4     cleanup1();
5     cleanup2();
6 }
7 

どちらも実行する必要があるなら、下記のように記述するべきだ。当たり前のことだが、しばしば上記のように書かれるので挙げておく。

 1 try {
 2     // do something
 3 } finally {
 4     try {
 5         cleanup1();
 6     } finally {
 7         cleanup2();
 8     }
 9 }
10 

次のようなコードは、不適切にもかかわらず意外とよく見かける。

 1static void writeTodayかなりいまいち(File file) throws IOException {
 2    try {
 3        OutputStream os = new FileOutputStream(file);
 4        OutputStreamWriter writer = new OutputStreamWriter(os, "UTF8");
 5        writer.write(new SimpleDateFormat("yyyy/MM/dd")
 6                .format(new Date()));
 7        writer.close();    // 上で例外が発生すると実行されない
 8        os.close();
 9    } catch (IOException e) {    // 非検査例外(RuntimeExceptionなど)のときに実行されない
10        file.delete();    // fileに書き込み権はないが削除権がある場合、osのオープンに成功したときのみ実行したいという意図に反して削除される。
11        logger.log(Level.SEVERE, e.getMessage(), e);    // ログの出力が重複している
12        throw e;
13    }
14}
15
16static void writeTodayCallerかなりいまいち() {
17    File file = new File("c:/tmp/tempfile.txt");
18    try {
19        writeTodayかなりいまいち(file);
20    } catch (IOException e) {
21        logger.log(Level.SEVERE, e.getMessage(), e);
22    }
23}
24

次のコードはかなりいいところまでいっているが、最後の基本原則が適用されていない。

 1static void writeTodayいまいち(File file) throws IOException {
 2    boolean done = false; // しばしばこうした変数の導入が必要になる
 3    OutputStream os = new FileOutputStream(file);
 4    try {
 5        OutputStreamWriter writer = new OutputStreamWriter(os, "UTF8");
 6        try {
 7            writer.write(new SimpleDateFormat("yyyy/MM/dd")
 8                    .format(new Date()));
 9            done = true;
10        } finally {
11            writer.close();
12        }
13    } finally {
14        os.close();
15        if (!done) {
16            file.delete(); // 上のcloseで例外が発生すると実行されない
17        }
18    }
19}
20
21static void writeTodayCallerいまいち() {
22    try {
23        writeTodayいまいち(new File("c:/tmp/tempfile.txt"));
24    } catch (IOException e) {
25        logger.log(Level.SEVERE, e.getMessage(), e);
26    }
27}
28

インデントが凸凹して複雑な構造になるが、以下が正しい例外の実装になる。

 1static void writeToday正解(File file) throws IOException {
 2    boolean done = false;
 3    OutputStream os = new FileOutputStream(file);
 4    try {
 5        OutputStreamWriter writer = new OutputStreamWriter(os, "UTF8");
 6        try {
 7            writer.write(new SimpleDateFormat("yyyy/MM/dd")
 8                    .format(new Date()));
 9            done = true;
10        } finally {
11            writer.close();
12        }
13    } finally {
14        try {
15            os.close();
16        } finally {
17            if (!done) {
18                file.delete();
19            }
20        }
21    }
22}
23
24static void writeTodayCaller正解() {
25    try {
26        writeToday正解(new File("c:/tmp/tempfile.txt"));
27    } catch (IOException e) {
28        logger.log(Level.SEVERE, e.getMessage(), e);
29    }
30}
31
inserted by FC2 system