Javaの非チェック例外

前置き

ある記事の中に、「catch ( Exception ex ) { ... } のようにcatchブロックを書くと、非チェック例外までcatchしてしまう可能性があるので良くない」というような主張が書いてありました。しかし、なぜ非チェック例外をcatchするのがマズイのかは書いてません。ということは、これは常識なんでしょうか。

ここでは、チェック例外と非チェック例外の使い分けについて考察してみたいと思います。

本文

例外の分類

代表的な例外クラスの継承階層を図示します。パッケージ名を省略したクラスは全て、java.langパッケージです。

例外のクラス階層

Javaの例外クラスはチェック例外と非チェック例外に分かれますが、継承関係と、チェック・非チェックの区別は、1対1に対応していません。上図のように、Errorクラス以下の派生クラスとRuntimeExceptionクラス以下の派生クラスが非チェック例外で、それ以外は全てチェック例外となります。RuntimeExceptionが、チェック例外と同じくExceptionから派生している点に注意して下さい。

チェック例外の「チェック」とは?

日本語では「チェック」ですが、英語では'checked'です。なので、「チェックされる例外」という意味でしょう。もっと丁寧に言うと、「コンパイル時に、ちゃんと処理されているかどうかをチェックされる例外」という意味です。この「ちゃんと処理されている」とは、以下の2つのどちらかを指します。

  • その例外をcatchする
public void foo() {
  try {
    // Socketオブジェクトを使って何かする。
  }
  catch ( SocketException ex ) {
    // 例外の内容を調べ、対処したりリトライする。
  }
}
  • その例外をthrows節に指定する
public void foo() throws SocketException {
  // Socketオブジェクトを使って何かする。
}

このうち、throws節に指定する方法は、厳密には「ちゃんと処理する責任を、メソッドの呼び出し側に押し付けている」だけです。コールツリーを遡り、どこかのメソッド内でcatchされて初めて「ちゃんと処理された」ことになります。

チェック例外の「チェック」には、プログラムがチェックする(つまりcatchする)という意味も含まれているようです。

チェック例外の使い道は?

Javaプログラマなら誰しも、単にコンパイラを黙らせるために、空のcatchブロック(しかもThrowableやExceptionをcatchするcatchブロック)を書いた覚えがあるでしょう。これはもちろん、good practiceとは言えませんが、気持ちは分かります。チェック例外の狙いを理解してない人にとっては、チェック例外は単に面倒くさいものという印象しか残らないと思います。

ではチェック例外の狙いとは何なのでしょう?

そもそも例外の目的は、メソッドの異常を呼び出し側へ伝えることです。C言語などでは、関数の戻り値として特殊な値(-1とか)を返すことにより異常を伝えることが多いですが、呼び出し側が戻り値のチェックをサボることも多いので、バグの温床となりがちです。そこでJavaでは、例外は原則としてチェック例外に分類し、チェックを義務付けているわけです。

Javaのメソッドがどんな例外をthrowするかという情報は、C言語の関数がどんな値をreturnするかという情報並みに重要であり、メソッドのI/F仕様に明記すべき情報です。よって、throwする側にはthrows節が義務付けられ、呼び出し側にはcatchが義務付けられているのです(原則として、ですが…)。

なぜ非チェック例外があるのか?

Javaの言語仕様では、「なぜErrorやRuntimeExceptionはチェックされないのか?」について以下のように説明してあります。

Errorは、プログラムのあらゆる場所で発生する可能性があり、発生した場合はリカバリが困難である。そういった例外のチェックを義務付けるとプログラムが煩雑になってしまう。

RuntimeExceptionのチェックを義務付けても、正しいプログラムの確立には大して寄与しない。またプログラマの目で見れば明らかにRuntimeExceptionが起きえないようなケースでも、コンパイラにはそれが分からないかもしれない。そんなケースにまで例外のチェックを義務付けるとプログラマを苛立たせてしまう。

歯切れが悪いですね。

私見

仮に、非チェック例外を、catchできない例外と考えてみたらどうでしょう? 例外がcatchされない場合、プログラムはabortしますよね。プログラムのabortが許されるのは、バグ、あるいは(メモリ不足のような)致命的状況の場合です。なので、非チェック例外の使いどころは、C言語のassert()に近くて、積極的にabortさせたいときです。つまり「もしここでassertionエラーが起きたらバグです」の精神です。assertionエラーは私(その関数の実装者)のバグですが、非チェック例外は私以外(呼び出し元や下位レイヤ)のバグです。いずれにしても、潔く落ちるべきです(隠蔽して動き続けようとすべきではありません)。

一方、チェック例外は、「いくらプログラムを正しく書いても、この例外が起きることは十分に考えられるので、(呼び出し側は)ちゃんとケアしほしい」というときに使います。

悩ましいのは、例外クラスごとにチェック・非チェックの区別が決まっているという点です。例えばSecurityExceptionは非チェック例外なので、これをチェック例外的に使うことはできません。チェック例外的に使いたいケースでは、Exceptionから派生した独自のMySecurityExceptionクラスを導入する必要があります。

また、非チェック例外群にとってのRuntimeExceptionに相当するクラスがチェック例外群には無いという点も問題です。チェック例外の共通の親となるクラスが無いので、1つのcatchブロックでチェック例外のみをまとめてcatchすることができません。よって、個々のチェック例外ごとに、同じようなcatchブロックを並べることになってしまいがちです。「前置き」にも書いた通り、Exceptionをcatchすると、非チェック例外までcatchされてしまうのでうまくいきません(Exceptionをcatch待ちしておいて、実際にcatchしたのがRuntimeExceptionだったら再throwする、というのはアリですが)。

いずれにしても、Javaの仕様は中途半端と感じます。コンパイラによるチェックは、プログラムの品質向上につながっていません。むしろ、コンパイルエラーを空のcatchブロックでもみ消して、見つけにくいバグの温床となってしまうかもしれません。また、安易にthrows節を追加することにより、コールスタックの根っこ側の(抽象レベルの高い)クラスが、下位レイヤのチェック例外に依存してしまうケースもあります。

結局はプログラマーのモラルやスキルで品質が左右されます。開発チーム内で例外に対する考え方を統一しておかないと、エライことになりますね。

Last modified:2011/01/10 00:33:24
Keyword(s):
References:[言語Tips]
This page is frozen.