例外(3)

例外処理は有効な機能ですが、使い方を誤ると発見しにくいバグを混入させてしまいます。
今回は例外処理(1)(2)で説明できなかった幾つかの機能と実装上の注意点を説明します。

finallyブロック

try/catch構文にはfinallyブロックを追加する事ができます。

    try
    {
        // 処理
    }
    catch(例外A e)
    {
        // 例外処理
    }
    finally
    {
        // finally処理
    }

finallyブロックはtry/catchの後ろ(一番最後)に1つだけ記述します。


このfinallyブロックの中に記述された処理は、処理が正常に終了した場合も例外が発生し例外処理が行われても最後に必ず実行されます。
したがって、ファイルをオープンした後のクローズ処理など、例外の発生の有無に関わらず行いたい処理を記述することができます。

不吉なコード

try/catch/finally構文には様々な不吉なコードが存在します。
それらは例外処理に関する知識不足がもたらす危険なコードです。

1. 例外の握り潰し

メソッドがチェック例外(RuntimeExceptionとError以外の例外)をthrowする可能性がある場合、呼び出し元ではtry/catch構文で例外処理を行うか、上位メソッドに例外を伝播しなくてはなりません。
ここで「コンパイルが通らないが例外を上位メソッドにthrowさせたくない」という理由から、try/catch構文が使われるケースがあります。
[例外握り潰しの例]

    try
    {
        foo();
    } catch(例外A e) {
       // なにもしない(コンパイルを通す為…)
    }

さて、プログラムの実行時になんらかの異常が発生したから例外がthrowされた訳で、その例外はどこかで処理されねばなりません。
しかし、上記のようなコードを書いたならば、発生した例外が「なかったこと」として処理が継続されてしまいます。
絶対に起こりえない場合もあると思いますが、その場合でもログに出力する・実行時例外にラップしてthrowしなおすなどの処理が必要です。
このような「例外の握り潰し」はアプリケーションの実行時などに問題が起きた場合に原因を追究する障害となります。

2. 複雑な例外処理

複雑な処理をcatchブロックに記述してはなりません。


例外処理も処理である限り、なんらかの例外が発生する可能性があります。
これは処理を記述する為にはコードを書く必要があり、コードを書けば必ず例外が発生する可能性がある為、避けられない現実です。
しかし、例外処理中に例外が発生してしまうと本来の例外は無視され、その例外がthrowされていきます。
すると、元の例外が解らなくなってしまうのです。


したがって、例外処理の実行時に例外が発生する可能性を少しでも減らすべきです。
すなわち、あまりに複雑な処理が例外処理に必要と感じたならば設計を見直すべきでしょう。

3. 非チェック例外のcatch

JavaではErrorとRuntimeExceptionは非チェック例外として定義されており、コンパイル時にチェックされません。
つまり、例外処理も原則としては行わずフレームワークなどに一任すべき例外でした。
したがって、実行時例外は原則としてcatchしないのですが、非チェック例外もまとめてcatchしてしまう忌々しい記述があります。

    try
    {
        // 処理
    } catch(Throwable e) {
        // 例外処理
    }

Throwableは全ての例外とエラーのスーパークラス(基底クラス)である為、非チェック例外(実行時例外とエラー)もまとめてcatchしてしまいます。

    try
    {
        // 処理
    } catch(Exception e) {
        // 例外処理
    }

Exceptionは全ての例外のスーパークラス(基底クラス)である為、非チェック例外(実行時例外)をまとめてcatchしてしまいます。


つまり、本来は処理すべきでないコーディング上のバグなどで発生した例外もまとめて処理されてしまい、問題が発生した場合の調査を困難にします。


また、これらがthrows句に記述された場合は目も当てられません。
もし、使用するメソッドがExceptionをthrowすると宣言されていたならば、Exceptionでcatchするか同じように上位メソッドにExceptionを投げる以外にコンパイルを通す術がなくなるのです。

4. return文

例外処理でreturn文を書く場合、その戻り値が妥当かどうかを入念に確認しなくてはなりません。
例えば、除算メソッドで割る数に0が指定された場合、例外をcatchして0を返してしまうとしましょう。

    public int divide(int a, int b) {
        try {
            return a / b;
        } catch(ArithmeticException e) {
            return 0;
        }
    }

このように書いた場合、ゼロ除算でArithmeticExceptionが発生した場合に0が戻り値として返ります。
しかし、これは正しい仕様なのなのでしょうか?


恐らくは例外が発生することだけを恐れて、コーディングしたに違いありません。
しかし、このような記述は通常は誤りです。
本当に結果が0である時と区別が付きませんし、本来ならばこのメソッドを実行する前にゼロ除算のチェックがあるべきです。


catchブロックにreturn文を書いてはならないというわけではありませんが、本当に返す値が妥当かの検討が必要です。
それがコーディング上の問題で発生するような例外(実行時例外)であるならば、そのまま上位メソッドに伝播するか、然るべき例外をthrowするべきでしょう。
尚、ArithmeticExceptionは実行時例外(非チェック例外)である為、本来は例外チェックを行う必要はありません。
メソッドのチェックとしては次のような形が望ましいと言えます。

    public int divide(int a, int b) {
        if(b == 0) {
            throw new IllegalArgumentException("b is null.");
// 仕様によっては0を返す場合もある
//            return 0;
        }
        return a / b;
    }
5. finallyの処理

finallyブロックはcatchブロック以上に記述に気を配らなくてはなりません。
catchブロックで問題が発生するのは例外が発生した時に限りますが、finallyブロックは正常に処理が行われた場合でも実行される為、より深刻なバグとなる可能性があります。


これはこれまで記述してきた不吉なコードが全て当てはまります。
finallyブロックは例外処理が行われた場合でも実行される為、例外を発生させた場合には元の例外を握りつぶしてしまいます。
また、return文をfinally句に書くことは危険です。
tryブロックで戻り値が作られても、例外が発生したとしても、finallyでreturn文が記述されていると、メソッドの最後の挙動はそのreturn文で制御されてしまうからです。