torutkのブログ

ソフトウェア・エンジニアのブログ

例外処理機構の使い方

最近、例外処理機構をアプリケーションの中でどのように使うかの議論をあちらこちらで見かけるようになりました。複数のプログラマで共同開発をするプロジェクトにおいては、Javaに限らず、例外処理機構を持つプログラミング言語を使う場合、エラー処理について事前に共通規約を定義しておかないと、出来上がるプログラムがエラー処理に関してとんでもない事態になってしまいます。

また、例外機構については、現時点でもまだ「ベストプラクティス」と言える統一見解がなく、戻り値によるエラー通知か例外によるエラー通知か、また例外を使う場合も、Javaのチェックされる例外(checked exception, 本日の日記では以下検査例外と表記)については賛否両論があります。

このあたり、Web上のブログや記事で参考になる言及のあるものをいくつかピックアップします。

Javaでのエラー処理を考える際のヒントになるところ

(2021/05/01追記)上述URLはリンク切れにつきinternet archiveのクロールURLを次に記載

ブログタイトルは.NETとJavaの違いなのですが、内容にはJavaにおいて検査例外、実行時例外(チェックされない例外のうちRuntimeExceptionとそのサブクラス)をどのように使い分けるか、アプリケーション開発の観点で提案されています。

Java には検査例外と実行時例外と呼ばれる 2 種類の例外が存在しており、言語仕様として業務エラーを例外(検査例外)として取り扱える仕組みを持っているからです。

なるほど、「業務エラー」をJava言語仕様の検査例外にマッピングするという提案です。次に、「業務エラー」について言及しているところを探すと、

この検査例外が、「必ず処理ルートとして考慮しなくちゃいけないケースである」ということを意味しており、この特徴はそのまま業務エラーに当てはまります。つまり、業務エラーとはそもそもどのようなものだったのかというと、
* メソッド側(上の例でいうと BC 側)では、インタフェース仕様(メソッド仕様)の一部として定義しなければならないもの。
* 呼び出し側(上の例でいうと UI 側)では、必ず後処理してメッセージなどを表示しなければならないもの。

とあります。業務処理を記述するときには、業務エラーに対する処理を記述する必要があり、そのためには、シグニチャにエラー通知が明示されており、また、業務エラー対処が抜けていた場合、コンパイルチェックができることがメリットとして挙げられています。

Error, RuntimeException, Exceptionの3つの例外クラス階層についての分析があります。

Javaの検査例外の是非

Josha BlockのEffective Javaにおける検査例外のヒント、Bruce EckelやRod Johnsonの検査例外への批判意見などを取り上げています。

DelphiC#の作成者Andres Hejlsbergの同題を前橋さんが訳したブログ。この内容によると、Hejlsberg氏は、検査例外の問題点としてスケーラビリティとバージョン問題を指摘しています。賛同はできないけれども。

そういえば、マイクロソフトは以前、オブジェクト指向プログラミングに対して、脆弱な基底クラスという問題定義をして否定的な意見を表明していたと思います。(継承を使うと、スーパークラスを変更するとサブクラス全体に変更が波及するという意味)

上のAndres Hejlsberg氏の記事へのJames Gosling氏の反論を前橋さんが訳したブログ。
スケーラビリティの問題指摘に対しては、例外ラッピングを、バージョン問題については例外翻訳を使うという回答をしています。

今の考え

C++言語を開発に使っていたいくつかのプロジェクトの経験から
  • エラーを戻り値で返す方針のプロジェクト

メンバー関数のシグニチャ定義方針が、戻り値は関数の実行結果を表し、関数の入出力はすべて引数で行う(in, outパラメータ)、となります。あるいはエラーを返すものと返さないものとでシグニチャ定義方針が異なってしまうかです。可読性が例外を使うものより劣化すると思っています。

戻り値は取得しなくてもコンパイルエラーにはならないので、メンバー関数呼び出し時に戻り値を取らないコードがはびこり、エラーを検出して対処しないまま動作を継続して結果事態が悪化して異常終了するという惨状がよく発生します。この場合、エラー検出をさぼった場所と異常終了場所が離れてしまうことが多く、原因特定に時間がかかってしまうことが多いものです。なんせ、C++はnullポインタにメンバー関数呼び出しをしてもそこでは落ちないのですから。

  • エラーを例外で返す方針のプロジェクト

メンバー関数呼び出し時に例外をキャッチしないコード、キャッチしていても、巨大なtry-catchを使用するコードがありました。C++のときは最上位(典型的にはmain関数内)で安全ネットとして例外を全取得していますが、よくそこまで例外が上がりました。もはやそこではまともな例外処理(回復を計る)はできず、ただメッセージを出して終了するのみです。

フレームワーク設計を行ったプロジェクトでは、mainまで上がらなくてもいいように、アプリケーション側で定義するクラスの継承元のクラスに例外を管理する機能を入れて、もう少し例外発生現場に近いところで安全ネットを設けました。

多階層設計をしていると、直接呼び出すメンバー関数の仕様で記述された例外をキャッチすることはあっても、その先の階層で発生した例外が何かまで調査してキャッチするなんてことはまずありえません。例外安全なコードを書く人も少数です。ちょっと難易度が高いですから。

巨大なtry-catchは、try節に書かれた複数のステートメントのどこで例外が発生したかを調べるのが大変です。最近関わったプロジェクトでは、例外クラスをnewするときに、その場所でのスタック情報を例外オブジェクトに持たせる方法を「Binary Hacks」を見て実装し、Javaのように例外オブジェクトを調べれば例外発生場所が特定できるようにしてこの問題をすこし軽減させました。

Javaでは

今のところの考え

  • 予期できるエラーでアプリケーション側でエラー対処をするものについては検査例外を使います
    • リトライで回復が期待できる事象など
  • 予期できるエラーでもアプリケーション側ではエラー対処しようがないもの(実行環境からスローされるErrorとそのサブクラス)には対処しません
    • メモリ不足など
  • インタフェース誤りのようなバグについては、assertか実行時例外を使います
    • assertは、事前条件/事後条件の検査
    • 実行時例外は、それ以外
    • 「publicなメソッドの事前条件検査にはassertは使わない」という意見については態度保留中
  • アプリケーションで定義する例外クラスは、Exceptionのサブクラスにアプリケーション例外の基底となるクラスをつくり、そのサブクラスとする