JDK1.4、JDK1.5 で IOException や SQLException の例外を投げたい場合どうするか

JDK1.6 から IOException や SQLException 新たなコンストラクタが追加された。

追記2009-11-17:
結論だけ先に書く。

// JDK1.6 の場合こう書けるが
throw new SQLException("数値のパースに失敗しました。", nfe);

// JDK1.4, JDK1.5 の場合、こう書くのが正解
SQLException sqle = new SQLException("数値のパースに失敗しました。");
e.initCause(nfe);
throw sqle;


以下、本文。

    /**
     * Constructs an {@code IOException} with the specified detail message
     * and cause.
     *
     * <p> Note that the detail message associated with {@code cause} is
     * <i>not</i> automatically incorporated into this exception's detail
     * message.
     *
     * @param message
     *        The detail message (which is saved for later retrieval
     *        by the {@link #getMessage()} method)
     *
     * @param cause
     *        The cause (which is saved for later retrieval by the
     *        {@link #getCause()} method).  (A null value is permitted,
     *        and indicates that the cause is nonexistent or unknown.)
     *
     * @since 1.6
     */
    public IOException(String message, Throwable cause) {
        super(message, cause);
    }

便利過ぎて涙が出てくるって話である。


しかし、現状うちの職場では JDK1.4 か JDK1.5 を使用しており、移行はまだ先になりそうだ。JDK1.4 なんてほんとはいつまでも使っていてはまずいのだが。


で、java.sql.Connection なんかの実装クラスを作った場合、SQLException を投げないといけないケースがあるのだが、そのような場合、JDK1.6 で上記コンストラクタが追加されるまでは、cause を引数に渡せなかったのである。


そういう場合どうするか。NumberFormatException nfe を受け取った catch 節の場合・・・。

// スタックトレースが途切れてしまう悪い例
SQLException e = new SQLException("大変な例外が起きました!ログにスタックを吐いておいたので、調べてください!");
logger.error(nfe);
throw e;

もちろんこれも無しとは言えない。ただ、ログに吐くかどうかは本来この処理の中では決めることはできない。呼び出し元処理の方で判断することである。java.sql.Connection みたいなどういう使われ方するか分からないような処理の中であることを考えると、いまいちだろう。ログをここではかない場合は、スタックトレースがここで途切れることになる。例外の隠蔽はエンジニアにとっては脱税の次くらいに重罪だ。

// JDK1.6 の場合こう書けるが
throw new SQLException("数値のパースに失敗しました。", nfe);

// JDK1.4, JDK1.5 の場合、こう書く
SQLException sqle = new SQLException("数値のパースに失敗しました。");
e.initCause(nfe);
throw sqle;

多分、上記が一番安全。2つのやり方で出力されるスタックトレースはまったく同一である。

// JDK1.4 以降、これもそれっぽいスタックトレースを出すが、問題あり。
SQLException sqle = new SQLException("数値のパースに失敗しました。");
sqle.setStackTrace(nfe.getStackTrace());

こういう書き方も似たようなスタックトレースを出すのだが、Exception 内に保持されている cause という private 変数が初期化されず、

sqle.getCause().getMessage()

みたいなことをされると NullPointerException が出てしまう。getStackTrace() は少し処理の重いメソッドだし、JDK1.5 で使うと現状少しずつメモリリークするしで、いいことが無いといえる。


initCause()しておけば、これの動作も当然本物と同じになり、

sqle.getCaluse().getCaluse().getCaluse().getCaluse().getMessage()

いくらでも大丈夫。


SQLException の Javadoc には、

     * The <code>cause</code> is not initialized, and may subsequently be
     * initialized by a call to the 
     * {@link Throwable#initCause(java.lang.Throwable)} method.

と書かれている。同じことができるのに JDK1.6 でコンストラクタが追加されたことを見ると、この仕様が分かりにくかったということなのだろう。