« シェルスクリプト:「.svn」ディレクトリを一括削除する | トップページ | Java:Reader/Writerにおけるclose()メソッド呼び出しの流儀 »

Java:例外が起こったときに実行されるコードとされないコード

【課題】
try〜catch〜finallyのうち,finallyブロックの中で例外が起こったときの挙動を改めて確認します。
finallyブロックについて,tryブロックの中で例外が発生してcatchされなかった場合でもfinallyブロックは必ず実行されることになっています。
この挙動について,いくつか疑問が生じます。今回はその疑問について,実際に検証コードを書いてどのような動作をするのか確認してみます。
(※本記事は2015年6月6日に全面改訂しました)

●問1) finallyブロックの中で例外が発生した場合どうなるのか。
「必ず実行される」finallyブロックですが,finallyブロックの中で例外が発生した場合,例外が発生した場所以降のコードは実行されるのでしょうか?
試してみましょう。テストコードは以下になります。

public class ThrowTest
{
  //------------------------------------------------------------------
  public static void main (String[] args)
  {
    try {
      ThrowTest.thrower();
    }
    catch (Throwable ex) {
      System.out.println("main:例外をキャッチしました");
      ex.printStackTrace();
    }
  }
  //------------------------------------------------------------------
  // 例外が発生するメソッド.
  private static void thrower ()
    throws Exception
  {
    try {
      String str = null;
      str.toString(); // tryの中で例外発生.
    }
    finally {
      String str = null;
      str.toString(); // catchの中で例外発生.
      System.out.println("finally:例外発生の後");
    }
  }
}
※上記コードでは,整形のため空白部分は全角スペースを使用しています。
実行結果
main:例外をキャッチしました
java.lang.NullPointerException
  at test.ThrowTest.thrower(ThrowTest.java:30)
  at test.ThrowTest.main(ThrowTest.java:12)
結論
「finally:例外発生の後」が出力されなかったので,finallyの中で例外が発生した場合,それ以降のコードは実行されませんでした。
考察
finallyブロックの中ですべての処理を確実に行わせるには,メソッドの呼び出しで発生する可能性があるすべての例外をfinallyブロックの中で確実にキャッチする必要があります。
一例として,ファイルの入出力でfinallyブロック内でcloseメソッドを呼ぶ場合,すべての行をtry〜catchで囲む実装が考えられます。
  Reader r = null;
  Writer w = null;
  try {
    … // ReaderからWriterへのデータ転送処理
  }
  finally {
    try { if (r != null) { r.close(); }} catch(IOException ex){ ex.printStackTrace(); }
    try { if (w != null) { w.close(); }} catch(IOException ex){ ex.printStackTrace(); }
  }
※上記コードでは,整形のため空白部分は全角スペースを使用しています。

●問2) tryブロックとfinallyブロックの両方で例外が発生したら,catchブロックはどちらの例外を受け取るか。
試してみましょう。問1のコードのうち,例外が発生するメソッドを以下のように書き換えます。

  //------------------------------------------------------------------
  // 例外が発生するメソッド.
  private static void thrower ()
    throws Exception
  {
    try {
      throw new Exception("tryブロックで例外発生");
    }
    finally {
      throw new Exception("finallyブロックで例外発生");
    }
  }
※上記コードでは,整形のため空白部分は全角スペースを使用しています。
実行結果
main:例外をキャッチしました
java.lang.Exception: finallyブロックで例外発生
  at test.ThrowTest.thrower(ThrowTest.java:33)
  at test.ThrowTest.main(ThrowTest.java:12)
結論
finallyブロックで例外が発生した場合,呼び出し元はfinallyの例外をcatchしました。
そしてtryブロックの例外はcatchできませんでした。
後から発生したfinallyブロックの例外が,先に発生したtryブロックの例外を上書きした形になりました。
考察
tryブロックの例外がfinallyブロックの例外に上書きされないようにするには,以下の2つの対応が考えられます。
 1) try〜finallyではなくtry〜catch〜finallyとし,tryブロックで発生した例外はcatchで受け取るようにする。
 2) 問1の考察で述べたのと同じく,finallyブロックの例外はすべてfinallyブロックの中でcatchする。
どちらを採用すべきかは,例外が発生したときに,それ以降の処理をどこまでスキップさせるかで変わってくるかと思います。
tryブロック内の処理だけをスキップするのであれば,1)を採用することになるかと思います。
tryブロック以降の処理もスキップするのであれば,2)を採用することになるかと思います。

●問3) try〜catch〜finallyの入れ子は可能なのか?
試してみましょう。問1のコードのうち,例外が発生するメソッドを以下のように書き換えます。

  //------------------------------------------------------------------
  // 例外が発生するメソッド.
  private static void thrower ()
    throws Exception
  {
    try {
      try {
        System.out.println("Start");
        throw new Exception("例外発生");
      }
      finally {
        System.out.println("内側のfinally:で実行");
      }
    }
    finally {
      System.out.println("外側のfinally:で実行");
    }
  }
※上記コードでは,整形のため空白部分は全角スペースを使用しています。
実行結果
Start
内側のfinally:で実行
外側のfinally:で実行
main:例外をキャッチしました
java.lang.Exception: 例外発生
  at test.ThrowTest.thrower(ThrowTest.java:24)
  at test.ThrowTest.main(ThrowTest.java:9)
結論
try〜catch〜finallyの入れ子は可能でした。
考察
この書き方は,以下のケースで使えるかと思います。
 1) try〜catch〜finallyではJavaの文法としてfinallyを先に書くことはできないが,先に書いたほうがコードが理解しやすくなるケース。
 2) 問2の考察で述べた,例外のcatchを先送りにするケース。
 3) 複数のtry〜finallyを最後の1箇所でまとめてcatchするケース。

●問4) tryブロックの中でreturnしたら,finallyブロックは実行されるのか?
試してみましょう。問1のコードのうち,例外が発生するメソッドを以下のように書き換えます。今回はtry〜finallyの入れ子の中でreturnしてみました。

  //------------------------------------------------------------------
  // 今回は例外は発生しません.
  private static void thrower ()
    throws Exception
  {
    try {
      try {
        System.out.println("Start");
        return; // ここでreturn.
      }
      finally {
        System.out.println("内側のfinally:で実行");
      }
    }
    finally {
      System.out.println("外側のfinally:で実行");
    }
  }
※上記コードでは,整形のため空白部分は全角スペースを使用しています。
実行結果
Start
内側のfinally:で実行
外側のfinally:で実行
結論
returnで終了した場合もfinallyは実行されました。
考察
処理の途中でreturnするケースとしてよくあるのが,入力値のチェックをして不合格だったら以降の処理を行わずにreturnで抜ける,というものがあるかと思います。
このケースではfinallyの処理は必要ない場合があります。
長大なメソッドを書いてしまった場合,メソッドの最後にfinallyがあるのを忘れる可能性が高いので注意が必要です。

【著作権表記】上記コードを含む本ブログのプログラムコードは,私的利用可,商用利用可,改変しての利用可です。利用の際に作者に許諾を得る必要はありません。

■関連書籍をAmazonで検索:[Java]
スッキリわかるJava入門 第2版(kindle版)

にほんブログ村 IT技術ブログへ にほんブログ村 IT技術ブログ プログラム・プログラマへ 人気ブログランキングへ ←この記事が役に立ったという方はクリックお願いします。


ドミノ・ピザ【PC向けサイト】

|

« シェルスクリプト:「.svn」ディレクトリを一括削除する | トップページ | Java:Reader/Writerにおけるclose()メソッド呼び出しの流儀 »

Java」カテゴリの記事

トラックバック


この記事へのトラックバック一覧です: Java:例外が起こったときに実行されるコードとされないコード:

« シェルスクリプト:「.svn」ディレクトリを一括削除する | トップページ | Java:Reader/Writerにおけるclose()メソッド呼び出しの流儀 »