Javaの検査例外は、呼び出し側で「どんなに注意しても防げない」異常系

注:本記事の内容はJavaで公式にドキュメントされているものではなく筆者の見解です。とはいえクラスを設計する上で有用な指針たり得ると思われるので公開したものです。


おさらい - 検査例外と非検査例外

Javaの例外クラスには「catchしないとコンパイルエラーになる」検査例外(チェック例外、checked exception)とそうでない非検査例外(非チェック例外、unchecked exception)があります。

検査例外は最近は嫌われる傾向がありC#では採用されていませんしAltJava言語も軒並み不採用、さらにはJavaの新しめのライブラリにも非検査例外しか投げないものが出てきていますが、適切に使えば安全なプログラミングのための強力な武器であり、検査例外の有意義さについては @irxground さんの Javaの検査例外の存在意義 をご覧ください。


例外クラスを自作する場合、検査と非検査どちらにする?

Javaの標準クラスには検査例外も非検査例外もあります。なんらかの基準で作り分けられているはず。標準的なプログラミングをするのなら自作の例外クラスでもそれに準じて検査か非検査かを選択するべきでしょう。ではその基準とは?

この基準が、公式規格に記載がありません。だから このブログ記事 のように


「呼び出し元が例外を処理できる」場合のみ検査例外を使い、それ以外はすべて実行時例外


と解釈している人は多く、これはEffective Javaにも同様の記述があります。しかしそれは標準ライブラリの指針とは違いそうです。だってDBに書き込もうとしてSQLException(検査例外)が飛んだときコードで回復できますかと言われれば、DB障害があるならどうあがいたって処理は失敗です。


検査例外は、呼び出し側の責任でない異常系

ここからが筆者の見解になりますが、検査例外とは呼び出し側で発生を避けられない発生する可能性がある以上必ず捕まえる必要がある例外であると理解するとJavaの標準例外クラスの使い分けとほぼ一致します。

有名どころの非検査例外で行くと、



  • NumberFormatException: Integer.parse()に渡す引数が正しいことを確認すれば発生しません。


  • NullPointerException: nullを適切に扱っていれば発生しません。


  • ArrayIndexOutOfRangeException: リストや配列のサイズを把握できていれば発生しません。

検査例外の大物ならば



  • IOException: ディスク満杯や故障をはじめとして、呼び出し側で防げない異常です。


  • SQLException: DBの障害もまた呼び出し側には防げません。


  • InterruptException: Thread.sleep()を書くと必ずcatchを要求されて鬱陶しいことこの上ないのですが、外部からの信号で要求された時間待てませんでした、という異常はどうしても起こりえることです。

というようにです。

呼び出し側の責任である=呼び出し側で回避できる異常ならcatchの記述がなかったとしてもそれはプログラマの確信の現れかもしれないのでコンパイラが口を出すことではありません。だから非検査です。


指針を守っていない標準例外クラス達

おそらくJava最初期の例外システム設計者はこの「呼び出し側に責任のない例外が検査例外」という指針を持っていたのではないかと思われますが、ドキュメント化されていなかったせいでしょう、後に追加されたパッケージでは必ずしも守られていません。


  • JPAのPersistenceException。DB障害があれば飛ぶのでSQLExceptionとよく似ているのですが非検査です。指針に沿うなら検査例外であるべきです。

  • JAXBのJAXBException。JAXBはクラス定義をリフレクションで解析してXMLパーサを作ってくれるユーティリティですが、不適切なクラス定義を読み込ませたときにこれが飛びます。NumberFormatExceptionと同類なので非検査であるべきですが、そうなっていません。


提言

非検査例外の基底クラスであるRuntimeExceptionという名前につられると非検査例外の意味を見失います。RuntimeExceptionという文字を見たら脳内でInvlidOperationExceptionと変換しましょう。すると適切な使い分けが自然にできるかと思います。


コメントへの回答 - 呼び出し側に部分的に責任のある例外

はてなブックマークでいい指摘をいただきました。


gowithyou

違うと思う。SQLExceptionは引数のSQLが間違えていたら、呼び出側の責任だよね?今迄の見解通り「回復可能性」で判断するのが妥当でしょ


SQLExceptionについてはまさにこの指摘通りの通り、もう少し厳密な議論の可能な例外でした。見出しの通り、SQL呼び出しが正常終了しないことには呼び出し側にも部分的に責任がありえます。すなわち、「文法的に間違ったSQL文を渡してしまった」場合などです。本稿の指針(そしておそらくJava例外の基本方針)に厳密に沿おうとすれば、SQL呼び出しは2種類に分けるべきとなります。


  • SQL文法や型、制約、その他のルール違反: 非検査例外。SQLStatementException とでも呼びましょうか。

  • DB障害、デッドロック発生、その他の避けようがない失敗: 検査例外。SQLRequestException とでも呼びましょうか。

ではなぜこの2種類に分かれていないのか、結論から言えば「SQLStatementExceptionが飛びうるメソッドは必ずSQLRequestExceptionも飛ばしうる」から結局絶対にcatchは書かないといけない、なら検査例外をひとつ用意すれば正しいロジックを強制するのに十分だからでしょう。

呼び出し側の責任が部分的という点ではあの例外もです。MessageDigest.getInstance(String アルゴリズム名) が飛ばすNoSuchAlgorithmException(検査例外)。JVM環境ごとに装備しているハッシュアルゴリズムセットは異なるので、そういう意味ではプログラムを書く時点で「アルゴリズム未実装」は避けようがない失敗だとも言えるのですが、実装が保証されているアルゴリズムも存在します。MD5, SHA-1/256/384/512あたりですね。この辺を指定して呼び出しているのにcatchを強制されるとストレスだけが残ります。実装が保証されているアルゴリズムについてはgetInstance経由でなく定数として提供するインターフェースであるべきでした。

さてこれらの考察があった上でコメントの「『回復可能性』で判断するのが妥当」かどうかと言われれば、やはりそんなことはないと考えます。「回復できる」とは何ができることなのかの定義がまずはっきりしていません(おそらくアプリの要件依存です)し、検査例外がcatch記述の強制をするものである以上、起こったにどう処置できるかでなく起こりうるか=catchが絶対に必要かという起こるのことが関心領域だからです。