try-catch-finallyのfinallyでthrowしない

前回の記事で「finallyブロックではreturnしない」ことを記載しました。これは「finallyブロックにreturn文を書くと戻り値が上書きされて直感的な動作ではなくなる」からです。

これとは別に、もう一つ同じように結果が上書きされて直感的な動作ではなくなるものがあります。それが「finallyブロックから呼び出し元に例外を投げる」です。

前回の記事を読んでいない方はぜひ前回の記事も参照ください。

tech-kodawari-japan.hatenablog.com

finallyブロックから呼び出し元に例外を投げない

finallyブロックの目的は利用したリソースのclose処理を実行することです。 しかし、リソースのclose処理は例外が発生するものが沢山あります。 そのため、finallyブロックでtry-catchをネストしてclose処理で発生した例外をcatchして対処します。

この時、close処理で発生した例外をcatchせずにthrows宣言を利用して呼び出し元へ投げると「例外の上書き」が発生します。

実際にコードを実行して動作を見てみましょう。

public static void main(String[] args) {
    // どの例外が発生するのか検証
    try {
        String actual = finallyThrow();
        System.out.println(actual);
    } catch (Exception e) {
        System.out.println("4. mainメソッドの例外ハンドリングで出力");
        System.out.println(e.getMessage());
        System.out.println(getStackTrace(e));
    }
}

private static String finallyThrow() throws Exception {
    try {
        System.out.println("1. 例外が発生する可能性のある処理");
        boolean isError = true; // 例外有無の制御用
        if (isError) {
            throw new Exception("処理中に例外発生");
        }
    } catch (Exception e) {
        System.out.println("2. catch:例外をキャッチして例外処理を実行");
        throw new Exception("catchして例外を投げる", e);
    } finally {
        System.out.println("3. finally:リソースのクローズなどを実行");
        // finallyの処理で発生した例外を呼び出し元に投げる(やってはいけない)
        boolean isFinallyError = true; // 例外有無の制御用
        if (isFinallyError) {
            throw new Exception("finallyで例外発生");
        }
    }

    // メソッドの戻り値
    return "methodのreturn";
}

// stackTraceを文字列にして返す
private static String getStackTrace(Exception e) {
    // close処理は割愛しています。
    StringWriter sw = new StringWriter();
    PrintWriter pw = new PrintWriter(sw);
    e.printStackTrace(pw);
    pw.flush();
    return sw.toString();
}

上記サンプルコードでそれぞれfinallyブロックの例外有無による違いを見てみます。

例外発生後にリソースがcloseできたパターン

1. 例外が発生する可能性のある処理
2. catch:例外をキャッチして例外処理を実行
3. finally:リソースのクローズなどを実行
4. mainメソッドの例外ハンドリングで出力
catchして例外を投げる
java.lang.Exception: catchして例外を投げる
    at inspection.FinallyThrow.finallyThrow(FinallyThrow.java:29)
    at inspection.FinallyThrow.main(FinallyThrow.java:11)
Caused by: java.lang.Exception: 処理中に例外発生
    at inspection.FinallyThrow.finallyThrow(FinallyThrow.java:25)
    ... 1 more

こちらは特に問題なく、業務処理の中で発生した例外が呼び出し元メソッドに投げられ、それをcatchしたmainメソッドでは、どこでどのような例外が発生したのかがちゃんと分かるスタックトレースが出力されています。

例外発生後にリソースのcloseで例外を投げたパターン

1. 例外が発生する可能性のある処理
2. catch:例外をキャッチして例外処理を実行
3. finally:リソースのクローズなどを実行
4. mainメソッドの例外ハンドリングで出力
finallyで例外発生
java.lang.Exception: finallyで例外発生
    at inspection.FinallyThrow.finallyThrow(FinallyThrow.java:35)
    at inspection.FinallyThrow.main(FinallyThrow.java:11)

finallyブロックから呼び出し元に例外を投げた場合、「業務処理で発生した例外の上書き」が起こります。

コンソールの出力を確認しても、業務処理で発生した例外について何も情報が出ておらず、finallyブロックのclose処理で発生した例外しか情報が出ていません。

try-catchを利用するということは、業務処理の中で例外が発生することが想定されます。 そして、想定通り業務処理で例外が発生したのであれば欲しい情報は「想定通り発生した業務例外の情報」であって、「例外により行ったclose処理の情報」ではありません。

そのため、finallyブロックで例外が発生した場合は呼び出し元に例外を投げるのではなく、finallyブロック内でちゃんとcatchして対処しましょう。

try-with-resourcesは例外どうなるの?

AutoCloseableインタフェース、もしくは、Closeableインタフェースを実装しているクラスは「try-with-resources」を利用することでJavaVMがリソースのclose処理を実行してくれます。リソース解放が「プログラマの責務」から「JavaVMの責務」となりますので活用しましょう。

さて、「try-with-resources」を使うと勝手にJavaVMが開放してくれるのは嬉しいのですが、JavaVMがリソースを解放しようとして例外が発生した時、その例外はどうなるのでしょうか。

先に結論から言うと「close処理で発生した例外は抑制」されます。そのため、例外の上書きは発生しません。「try-with-resources」を利用した場合、リソースの開放漏れ、リソース解放時の例外に対してプログラマは何もしなくて良いです。

素晴らしいですね!!責務がちゃんとJavaVMに移っています。プログラマは細かいことは気にしなくて良くなっています!

本当にリソースがちゃんと開放できたのか知りたい

「try-with-resources」を使うとJavaVMが勝手にやってくれるとは言ったものの、場合によってはリソースの開放をちゃんと見届けたい事もあるかもしれません。

例えば、

プロジェクトで固有のリソースクラスを作成していて、そのリソースの開放が失敗してしまうと結構大きなリソースリークが発生する可能性がある。だからちゃんとリソースが開放できたかを把握したい。

こんな場合はどうすればよいのでしょうか?

そんな時は「Throwable.getSuppressed()」を実行しましょう。抑制された全ての例外を取得することが可能です。 close処理でどんな例外が発生していたかをチェックすることができますので、ちゃんとリソースが開放されているか見届けることができます。

詳細は以下に公式ドキュメントのリンクを貼ります。気になった方は参照ください。

Throwable (Java SE 17 & JDK 17)

Chapter 14. Blocks, Statements, and Patterns

The try-with-resources Statement (The Java™ Tutorials > Essential Java Classes > Exceptions)

finallyブロックで例外をcatchして対処する

最後にちゃんとfinallyブロックで例外に対処した場合も見てみましょう。

public static void main(String[] args) {
    // どの例外が発生するのか検証
    try {
        String actual = finallyCatch();
        System.out.println(actual);
    } catch (Exception e) {
        System.out.println("5. mainメソッドの例外ハンドリングで出力");
        System.out.println(e.getMessage());
        System.out.println(getStackTrace(e));
    }
}

private static String finallyCatch() throws Exception {
    try {
        System.out.println("1. 例外が発生する可能性のある処理");
        boolean isError = true; // 例外有無の制御用
        if (isError) {
            throw new Exception("処理中に例外発生");
        }
    } catch (Exception e) {
        // 例外をキャッチした後にエラーを表す戻り値を返す
        System.out.println("2. catch:例外をキャッチして例外処理を実行");
        throw new Exception("catchして例外を投げる", e);
    } finally {
        System.out.println("3. finally:リソースのクローズなどを実行");
        // finallyの処理から例外が発生したけど例外をcatchして対処する
        try {
            boolean isFinallyError = true; // 例外有無の制御用
            if (isFinallyError) {
                throw new Exception("finallyで例外発生");
            }
        } catch (Exception e) {
            System.out.println("4. finallyの例外ハンドリングで出力");
            System.out.println(e.getMessage());
            System.out.println(getStackTrace(e));
        }
    }

    // メソッドの戻り値
    return "methodのreturn";
}

// stackTraceを文字列にして返す
private static String getStackTrace(Exception e) {
    // close処理は割愛しています。
    StringWriter sw = new StringWriter();
    PrintWriter pw = new PrintWriter(sw);
    e.printStackTrace(pw);
    pw.flush();
    return sw.toString();
}

実行結果

1. 例外が発生する可能性のある処理
2. catch:例外をキャッチして例外処理を実行
3. finally:リソースのクローズなどを実行
4. finallyの例外ハンドリングで出力
finallyで例外発生
java.lang.Exception: finallyで例外発生
    at inspection.FinallyCatch.finallyCatch(FinallyCatch.java:37)
    at inspection.FinallyCatch.main(FinallyCatch.java:11)

5. mainメソッドの例外ハンドリングで出力
catchして例外を投げる
java.lang.Exception: catchして例外を投げる
    at inspection.FinallyCatch.finallyCatch(FinallyCatch.java:30)
    at inspection.FinallyCatch.main(FinallyCatch.java:11)
Caused by: java.lang.Exception: 処理中に例外発生
    at inspection.FinallyCatch.finallyCatch(FinallyCatch.java:25)
    ... 1 more

finallyブロックで発生した例外をcatchしてコンソールに情報を出力しました。 ちゃんと例外に対処してからfinallyブロックを終了したため、mainメソッドのcatchブロックでは業務処理で発生した例外を処理できています。

まとめ

finallyブロックから呼び出し元に例外を投げてしまうと業務処理で発生した例外を上書きしてしまいます。

finallyブロックで発生した例外はちゃんとハンドリングしましょう。また、finallyブロックで例外を出すことを予防するため、finallyブロックにロジックを記述することは極力避けてリソースのclose処理だけ行いましょう。

めんどくさいことを考えたくないので「try-with-resources」を活用しましょう!