オブジェクト指向超入門〜第9回〜

例外はエラーの発生を無視できないよう強制する為の機能だと書きました。これをうっとおしいと感じるプログラマは多いようです。

ここに、AAExceptionを投げるfuncA()、BBException()を投げるfuncB()、CCException()を投げるfuncC()の3つの関数があって、それを順に呼びたいとします。まじめに書くと以下のようになります。

 try {
   funcA();
   funcB();
   funcC();
 } catch(AAException) {
   // エラー処理
 } catch(BBException) {
   // エラー処理
 } catch(CCException) {
   // エラー処理
 }

このように全ての例外をちゃんとcatchするには、それぞれの関数でどんな例外が投げられるのか、APIドキュメントを1つひとつ調べながらコーディングする必要があります。それに同じようなcatchブロックを何回も書かないといけません。これはメンドイって事で、以下のようにサボってしまうプログラマが少なからずいます。

 try {
   funcA();
   funcB();
   funcC();
 } catch(Exception) { // 3つの例外をまとめてキャーッチ!
   // エラー処理
 }

このやり方は正しいでしょうか。どの例外が発生してもエラー処理で行うべき対処が同じなのであれば、これで構わないように思えます。しかし、これはやってはいけません。面倒であってもちゃんと例外の種類ごとにそれぞれcatchしてやる必要があります。繰り返します。
Exceptionでまとめてキャーッチは絶対やっちゃ駄目!

何故でしょう?
このコードで発生する例外は3種類であるように見えますが、実はもっと多くの種類の例外が発生する可能性があります。言語の仕様にもよりますが、プログラマが意図しない想定外の例外が発生する可能性が常にあるのです。

そもそも例外が投げられる可能性があるという場合、それはあらかじめ想定された事態だと言えます。例えばファイルから値を読み込んで変数に代入する処理があったとして、そのファイルが存在しない等でオープンに失敗する可能性は、コーディング時にあらかじめ想定しておくべき事象です。だからオープンできなかった場合は throw IOException() と書いておくのですね。
ところが throw 〜Exception(); なんてどこにも書いてないはずの処理中に思わぬ例外が発生する事があります。いわゆる実行時例外です。NullPointerとかですね。この手の例外はキャッチしてはいけないんです。

は!?キャッチしちゃいけないって、どういう事?
もし3つの関数内のどこかでNullPointerが発生した場合、後者のコードではそれもキャッチします。そしてエラー処理をした後に後続の処理を続ける事になりますが、それは危険な事ではないでしょうか。このコードでのcatchはNullPointerの可能性まであらかじめ想定していたでしょうか。想定していたのは3種類の例外だけで、それ以外はいわば想定外の事態なはずです。なのにプログラムの実行を続けてしまって本当に大丈夫なのでしょうか。本来ならファイルから読み込んだ値がセットされているはずの変数が、例外発生により処理がスキップされてしまった為におかしな値が入った状態のまま実行を続けた結果、ずっと後の方になって思わぬ不具合が起こったとしたら...これは非常にタチの悪いバグです。
変数に不正な値がセットされる原因となった箇所と、不具合の事象が発生した箇所が遠く離れてしまうと、バグの調査が難しくなります。そもそも変数の値が不正になる直接の原因が何なのかを特定するのも極めて困難です。というかNullPointerが発生したという事にすら気付けません。

Exception(全ての例外の基底クラス)をキャッチするという事は、想定外の例外が発生した場合でも、その事実をもみ消してしまう事を意味するのです。だからまとめてキャッチ、っていうサボリはしてはいけないんです。じゃあ実行時例外が発生した場合はどうするのか。私に言わせれば、いさぎよくプログラムを落としてしまってもいいくらいです。

例外を面倒で邪魔くさいもんだと思わないであげて下さいね。