第4章 逆引きカタログ その他
Authors:Agata Toshitaka
ここまでは今までのJavaの常識からはありえない「実行時例外を使いましょう」という考え方をご紹介します(ちなみにこれは『実践J2EEデザイン』という書籍で紹介されたイディオムで、デザインパターンではありません)。
筆者も「実行時例外を使おう」と初めて聞いたときはたいへん驚きました。今までの常識が崩れ去った気分でした。
しかし、今ではこの方法で、問題なく、以前よりも効率良くアプリケーションを書いています。
例外には大きく分けて検査例外と実行時例外があります。
検査例外はjava.lang.Exceptionを継承した例外です。
検査例外が発生する場所では、必ずcatchブロックで例外をキャッチするか、throws句で例外をメソッドの呼び出し元に投げる宣言をする必要があります。
実行時例外はjava.lang.RuntimeExceptionを継承した例外です。
実行時例外は検査例外と違い、キャッチする必要はありません。
今までのJavaの常識は「検査例外を使いましょう。そうすれば堅牢なアプリケーションが書けますよ」でした。
確かに、検査例外を使うことで、メソッドの呼び出し元に例外のキャッチを強制することができ、「例外処理を書き忘れた」ということをなくせます。
しかし、みなさんの周りのアプリケーションを見てみてください。
あちこちに例外をキャッチするためのコードが書かれていて、しかもそのほとんどは「例外を別の例外に変換して投げ直すだけ」ということが多いと思います。
このようなルーチンワーク的な例外処理に意味はあるのでしょうか?
また「throwsApplicationException」のように多くのメソッドで「検査例外出ます。キャッチよろしく」と宣言されます。
これはインタフェースと相性がよくありません。
さまざまな実装が予想されるインタフェースのメソッドに「ハードコード」されたthrows句は、後々の汎用性の低下につながる場合があります。
結局、「呼び出し元が例外を処理できる」場合のみ検査例外を使い、それ以外はすべて実行時例外を使ったほうが、アプリケーションのコードがずいぶんシンプルになるということです。
特に通常のWebアプリケーションでは、例外をキャッチして何らかの処理ができることは稀でしょう。
途中のクラスで例外処理をする必要はなく、サーブレットなど大本の呼び出し元で「RuntimeException」をキャッチするだけでオッケーです。
それでは検査例外を使ったコードと、実行時例外を使ったコードの具体的な例を比較してみましょう。
検査例外を使った場合のコード(
リスト6,
リスト7,
リスト8,
リスト9)では、LogicAクラスのexecute()メソッドでDaoAクラスから投げられるSQLExceptionをキャッチして、ApplicationExceptionに変換して投げ直しています。
DaoAクラスのupdate()メソッドを使うほとんどの場所で、このような中途半端な例外処理を書く必要が出てきます。
リスト6 ServletA.java(検査例外版)
public class ServletA extends HttpServlet {
Log log = LogFactory.getLog(ServletA.class);
protected void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
try {
LogicA logic = new LogicA();
logic.execute();
} catch (ApplicationException e) {
log.error(e.getMessage(), e);
}
}
}
リスト7 LogicA.java(検査例外版)
public class LogicA {
void execute() throws ApplicatonException {
try {
DaoA dao = new DaoA();
dao.update();
} catch (SQLException e) {
throw new ApplicationException(e);
}
}
}
リスト8 DaoA.java(検査例外版)
public class DaoA {
Connection conn;
void update() throws SQLException {
conn.createStatement();
}
}
リスト9 ApplicationException.java(検査例外版)
public class Application extends Exception {
public ApplicationException(Throwareble t) {
super(t);
}
}
実行時例外を使った場合のコード(
リスト10,
リスト11,
リスト12,
リスト13)では、DaoBクラスのupdate()メソッド内で、SQLExceptionをその場でキャッチして、実行時例外SQLRuntimeExceptionに変換して投げています。
実行時例外ですので、LogicBクラスのexecute()メソッドではキャッチの必要もなくシンプルなコードになります。
ロジッククラスのexecute()メソッドから「throwsApplicationException」が消えたのもポイントです。
これにより、ロジッククラスのexecute()メソッドを呼び出すコードもシンプルになるはずです。
最終的にSQLRuntimeExceptionはServletBでキャッチされて、エラー画面へ遷移という「意味のある例外処理」が行われることになります。
リスト10 ServletB.java(実行時例外版)
public class ServletB extends HttpServlet {
Log log = LogFactory.getLog(ServletB.class);
protected void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException {
try {
LogicB logic = new LogicB();
logic.execute();
} catch (RuntimeException e) {
log.error(e.getMessage(), e);
}
}
}
リスト11 LogicB.java(実行時例外版)
public class LogicB {
void execute() {
DaoB dao = new DaoB();
dao.update();
}
}
リスト12 DaoB.java(実行時例外版)
public class DaoB {
Connection conn;
void update() {
try {
conn.createStatement();
} catch (SQLException e) {
throw new SQLRuntimeException(e);
}
}
}
リスト13 SQLRuntimeException.java(実行時例外版)
public class SQLRuntimeException extends RuntimeException {
public SQLRuntimeException(SQLException e) {
super(e);
}
}
◎ほんとに実行時例外で大丈夫なの?
大丈夫です。
実行時例外をアプリケーション全般に使用したほうがよいかどうかはアプリケーションの特性によって異なります。
通常のWebアプリケーションならば、実行時例外の使用は問題がないはずです。
ただし、2つのメソッドをまたぐトランザクションのロールバックなどに関しては、工夫する必要があるでしょう。
◎Exception→RuntimeException
1つの検査例外に対応した実行時例外を作りましょう。
筆者は、検査例外のクラス名のExceptionの部分をRuntimeExceptionに変更した実行時例外クラスを作って使用しています。
たとえば「ClassNotFoundException」なら「ClassNotFoundRuntimeException」です。
もちろんClassNotFoundExceptionが発生する場所では、その場でキャッチしFoundRuntimeExceptionに変換して投げ直します。
だって、ClassNotFoundExceptionを呼び出し元に返したって、何もできるはずありませんもんね。