Java 例外処理 Exception Handling

職場で、「例外処理どうしてます?」ということをきかれた。
自分の経験とか調べたことを書いておこう。

例外処理

アプリケーション実行中に、なんらかの問題が発生することがある。

問題が発生したんであれば、問題があったという事実を確実に知っておきたい。実装中でも、運用中でも、問題があるのに何も気づけないのは困る。

正常に動作しているうちは、それを専門とするオブジェクトにおまかせだけど、異常が発生したら、呼び出し元でも知っておきたいというはなしだ。

Javaでは、例外という仕組みをつかって、なんからの問題、「障害が発生した!」ということを検出することができる。ソースコードで、問題が発生した箇所で、問題を、例外(Exceptionクラス)というもので表して、それを呼び出し元に投げる。呼び出し元というのは、そのメソッドを直接呼んだメソッド、あるいは呼び出し元をたどって、最終的には、mainメソッドとか、スレッドのrunメソッドとかになる。呼び出し元では、catchをつかって、例外の種類に応じた例外処理を記述する。

例外がないプログラミング言語だと、メソッドを呼んでいる箇所で、メソッドを実行するたびに結果の正常・異常をif文などで、判定することになってめんどうくさいらしい。そりゃー、メソッドの呼び出しが深くなるとつらくなるな。そう考えると、Javaのような例外処理の仕組みはありがたい。

Exception(チェック例外)とRuntimeException(非チェック例外)

Javaの入門書やら教科書をみると、形式的に、例外の種類と違い、使い方が説明がされていることが多い。

googleなどで、もうちょっと、著者の意見がはいったブログなりなんなりを探して読んでみると、Exception(チェック例外)とRuntimeException(非チェック例外)のどちらが良いか?なんてのが書いてある。
「チェック例外不要!意味なし!」「catchを強制=例外処理を書くこと強制しなきゃだめ」みたいなことが書いてある。

Javaの理論と実践: 例外をめぐる議論

折衷案なのかどうかわからないが、RuntimeException(を継承したクラス)をthrowする場合は、メソッドにthrowsを書かなくてもよいのだけど、あえてそれを書くというスタイルもあるようだ。メソッドのシグニチャとしては、例外の発生を明示しつつ、ただし、メソッドの呼び出し元では、RuntimeException(を継承したクラス)なので、catchしなくてもいいというそんなスタイル。

Javaより後発で、当初は、Javaに似ているといわれたC#にも例外が利用できる。C#の例外は、Javaのようにチェックと非チェックにわかれていない。チェック例外はない。

Javaより後発の、JavaVM上で動くScalaも例外が利用できる。こちらもチェック例外はない。

一方で、Effective Javaなどを読むと、手抜きしないでチェック例外使いましょうみたいなことが書いてあったと思う。

プログラミング言語の開発者は採用を見送っているが、Javaらしいのはチェック例外をつかうスタイルということなんだろうか。経験上、手間なので、やっぱりあまりつかいたくない。それで、自分の書くJavaのプログラムでは、チェック例外は、すぐにcatchして、RuntimeExceptionにおきかえてしまっている。

Exceptionの復習

Oracle Technology Network for Java Developers | Oracle Technology Network | Oracle

教科書的に、例外の種類というかクラスの階層を示すと

  • Throwable
    • Exception
      • RuntimeException
    • Error
Throwable
例外のスーパークラス
Exception
例外
チェック例外。例外が発生することをメソッドのthrowsで明記する。明記されているおかげで、実行しなくても、ソースコードを書いてる段階で、というかコンパイルした段階で、ある種の異常(FileNotFoundExceptionだのSQLExceptionだの)が発生する可能性があることがわかるので、ソースコードにcatchを書いてそれに備える。いいかえると、メソッドの呼び出し元のメソッドでは、catchして、なんらかの対応をする必要がある。catchしないなら、呼び出し元でもthrowsすることになる。
  • FileNotFoundException
  • SQLException

Oracle Technology Network for Java Developers | Oracle Technology Network | Oracle

RuntimeException
実行時例外
実行時というのは、実行してはじめて、例外が発生することがわかるということ。throwsには書かない。したがって、catchしなくてよい(catch自体はできる)。というかそもそも実行時にこいつがでないように、開発中にテストをして、プラグラマーが実装なり、環境なりをなおすべき。

Oracle Technology Network for Java Developers | Oracle Technology Network | Oracle

Error
エラー

たとえcatchしてもなにもできなさそうな、文字通りの、処理を継続できなさそうな異常事態発生。

Oracle Technology Network for Java Developers | Oracle Technology Network | Oracle

  • OutOfMemoryError
  • ExceptionInInitializerError
  • NoClassDefFoundError
  • NoSuchMethodError

自分が実装するときの方針と例

自分は、「チェック例外を出すメソッドを実行するところで、とにかくcatchして、RuntimeExceptionかそのサブクラスでラップして、スロー」というやり方でいつもやっている。

catchしないで、メソッドにthrowsを書く方法は、throwsを何度も書いて、実装がめんどうだから。RuntimeExceptionなら、throws書かなくてすむから。例外は、いつでも発生するという心構えで、必ず実行したいようなメソッドは、たとえば、closeのようなメソッドを実行したいときは、finallyに書く。リソースのcloseは、例外のcatchとは無関係に、とにかくfinallyに書く。

やっぱり、いわゆる「決め」というやつで、なんらかの方針をつくって、それにしたがうといいんだろうか。

例えば、レイヤ間のやりとりをあらわすシーケンス図みたいなものを書いて、「ここで発生する例外は、ここでcatchして、ここで、こうやってハンドリングする」ということを検討しつつ、実装をすすめるとよいかもしれない。

catchしたとして、何をするのか?

catch処理の内容としては、

  • 異常ログ出力

がほとんど。同じ例外を何度もcatchして、その度、ログ出力、再スローみたいな実装だと、同じスタックトレースが何度もログ出力されて、ログファイルも見づらくてしょうがない。

これ以外だと、

  • 戻り値に適当な値を設定
  • データベースのトランザクション処理でロールバック
  • Webなどのサーバで、クライアントにエラーであることを示すレスポンスを返す
    • エラーページへ遷移(するための処理)
  • Webなどのクライアントで、サーバへリトライ

ぐらいしか経験がない気がする。フレームワークが面倒みてくれて、例外ハンドラのいうなものが準備されていれば、自分では書かないし。

よく経験するExceptionといえば、

NullPointerException
とにかく実装をなおさなきゃ。経験長くなるにつれて、減ってきたけど、それでもだしてしまう。^_^;
SQLException
DBの接続ミス。SQL文のまちがい。
FileNotFoundException
ファイルのおきわすれ。ファイルパスの記述ミス。

とかで、catchしてなんとかというより、それ以前になんとかしなきゃいけないようなものばかりだと思う。

以下のようなクラスで、かんたんな例をあげてみる。

  • OrderAction
  • OrderLogic
  • OrderDao
  • Order
  • SqlUtils
  • SystemRuntimeException
  • ApplicationRuntimeException

見てもらう順序は、

  1. SqlUtils
  2. OrderDao
  3. OrderLogic
  4. OrderAction

のほうが良いか?

package myapp.exception.example;

import java.sql.SQLException;
import java.util.List;

public class OrderAction {

    private OrderLogic orderLogic;

    // OrderLogic#doBusinessLogicA1が
    // SQLExceptionを発生する可能性があることはわかっている。
    // どう例外処理してよいかわからないので、executeA1メソッドを
    // 素通りさせる。それで、throws SQLException
    // そのほかの発生するかわからない例外のためには、
    // catchもthrowsも書かなくて良い。
    //
    // 発生する可能性がわかっている例外もそうでない例外も
    // OrderAction#executeA1の呼び出し元での例外処理に期待大!
    // それは、mainメソッドなのか、Servlet#doXXXXなのか、
    // フレームワークなのか。。。
    public void executeA1() throws SQLException {

        try {
            List<Order> list = this.orderLogic.doBusinessLogicA1();

            //画面表示とか?

        } catch (SQLException e) {
            // 例外処理を記述
            // catchしてそのまま再スローではあまりにも意味がなさすぎる。
            throw e;
        }
    }

    // OrderLogic#doBusinessLogicA2が
    // SQLExceptionを発生する可能性があることはわかっている。
    // どう例外処理してよいかわからないので、executeA2メソッドを
    // 素通りさせる。それで、throws SQLException
    // そのほかの発生するかわからない例外のためには、
    // catchもthrowsも書かなくて良い。
    //
    // この例では、発生する可能性がわかっている例外もそうでない例外も
    // OrderAction#executeA2の呼び出し元での例外処理に期待している。
    // それは、mainメソッドなのか、Servlet#doXXXXなのか、
    // フレームワークなのか。。。
    public void executeA2() throws SQLException {
        List<Order> list = this.orderLogic.doBusinessLogicA2();

        //画面表示とか?

    }

    // 発生するかわからない例外のためには、
    // catchもthrowsも書かなくて良い。
    //
    // この例では、発生する可能性がわかっている例外もそうでない例外も
    // OrderAction#executeBの呼び出し元での例外処理に期待している。
    // それは、mainメソッドなのか、Servlet#doXXXXなのか、
    // フレームワークのなのか。。。
    public void executeB() {
        List<Order> list = this.orderLogic.doBusinessLogicB();

        //画面表示とか?

    }

}
package myapp.exception.example;

import java.sql.SQLException;
import java.util.List;

public class OrderLogic {

    private OrderDao orderDao;

    // OrderDao#findAがSQLExceptionを発生する可能性があることはわかっている。
    // どう例外処理してよいかわからないので、doBusinessLogicA1メソッドを
    // 素通りさせる。それで、throws SQLException
    // ApplicationRuntimeExceptionが発生するかもしれないけど、
    // throwsでは宣言せず。こっそりだす?
    // いや、このシステムでは約束事なんですよ。
    // LogicクラスでApplicationRuntimeExceptionがでるかもしれないことは。
    public List<Order> doBusinessLogicA1() throws SQLException {

        try {
            List<Order> list = this.orderDao.findA(1L);
            validate(list);
            return list;
        } catch (SQLException e) {
            // 例外処理を記述
            // catchしてそのまま再スローではあまりにも意味がなさすぎる。
            throw e;
        }
    }

    // OrderDao#findAがSQLExceptionを発生する可能性があることはわかっている。
    // どう例外処理してよいかわからないので、doBusinessLogicA2メソッドを
    // 素通りさせる。それで、throws SQLException
    // ApplicationRuntimeExceptionが発生するかもしれないけど、
    // throwsでは宣言せず。こっそりだす?
    // いや、このシステムでは約束事なんですよ。
    // LogicクラスでApplicationRuntimeExceptionがでるかもしれないことは。
    public List<Order> doBusinessLogicA2() throws SQLException {
        List<Order> list = this.orderDao.findA(1L);
        validate(list);
        return list;
    }

    // OrderDao#findBが例外を発生する可能性があるかどうかは、
    // OrderDaoの実装を見ないとわからない。
    // 発生するかわからない例外のためにcatchもthrowsも書かなくて良い。
    // ApplicationRuntimeExceptionが発生するかもしれないけど、
    // throwsでは宣言せず。こっそりだす?
    // いや、このシステムでは約束事なんですよ。
    // LogicクラスでApplicationRuntimeExceptionがでるかもしれないことは。
    public List<Order> doBusinessLogicB() {
        List<Order> list = this.orderDao.findB(1L);
        validate(list);
        return list;
    }

    // ApplicationRuntimeExceptionが発生するかもしれないけど、
    // それは、約束事ということで、throwsには書かない。
    private void validate(List<Order> orderList) {

        // orderListの検証処理
        if (orderList.size() == 0) {
            throw new ApplicationRuntimeException("このデータは、業務的におかしくね?");
        }

    }

}
package myapp.exception.example;

import java.sql.SQLException;
import java.util.List;

public class OrderDao {

    //SqlUtils#selectがSQLExceptionを発生する可能性があることはわかっている。
    // どう例外処理してよいかわからないので、findAメソッドを
    // 素通りさせる。それで、throws SQLException
    public List<Order> findA(Long id) throws SQLException {
        return SqlUtils.select(Order.class, "select * from order where date = ?",
                id);
    }

    //SqlUtils#selectがSQLExceptionを発生する可能性があることはわかっている。
    //チェック例外(SQLException)を非チェック例外(RuntimeExceptionのサブクラス)
    //に置き換える。そして、fnidBメソッドの呼び出し元では、catchしなくてすむようにする。
    //ただし、SystemRuntimeExceptionが発生する可能性があることは
    //呼び出し元ではわからない。
    //Javadocに、ドキュメントとして明記するのがよいらしい。
    public List<Order> findB(Long id) {
        try {
            //
            return SqlUtils.select(Order.class,
                    "select * from order where date = ?", id);
        } catch (SQLException e) {
            throw new SystemRuntimeException(e);
        }
    }

}
package myapp.exception.example;

import java.util.Date;

public class Order {

    private Long id;

    private Date date;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Date getDate() {
        return date;
    }

    public void setDate(Date date) {
        this.date = date;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((date == null) ? 0 : date.hashCode());
        result = prime * result + ((id == null) ? 0 : id.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Order other = (Order) obj;
        if (date == null) {
            if (other.date != null)
                return false;
        } else if (!date.equals(other.date))
            return false;
        if (id == null) {
            if (other.id != null)
                return false;
        } else if (!id.equals(other.id))
            return false;
        return true;
    }

    @Override
    public String toString() {
        return "Order [id=" + id + ", getId()=" + getId() + ", getClass()="
                + getClass() + ", hashCode()=" + hashCode() + ", toString()="
                + super.toString() + "]";
    }

}
package myapp.exception.example;

import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

public class SqlUtils {

    //DB処理なのでSQLExceptionが発生します。
    //このことを
    //throws SQLException
    //として堂々宣言する!
    public static <T> List<T> select(Class<T> clazz, String sql, Object... args)
            throws SQLException {
        List<T> list = new ArrayList<T>();

        // DB処理があります。書かないけど。

        return list;
    }
}
package myapp.exception.example;

/**
 * 実装や設定のミス、サーバの異常などの例外
 */
public class SystemRuntimeException extends RuntimeException {

    /** serialVersionUID */
    //スーパークラスのjava.lang.Throwableがjava.io.Serializableを
    //implementsしてるので、宣言しないとEclipseでは警告がでる。
    private static final long serialVersionUID = -3766096732159234012L;

    public SystemRuntimeException(Exception e) {
        super(e);
    }

}
package myapp.exception.example;

/**
 * 業務ロジックの例外
 */
public class ApplicationRuntimeException extends RuntimeException {

    /** serialVersionUID */
    //スーパークラスのjava.lang.Throwableがjava.io.Serializableを
    //implementsしてるので、宣言しないとEclipseでは警告がでる。
    private static final long serialVersionUID = 5314434629880746832L;

    public ApplicationRuntimeException(String message) {
        super(message);
    }
}

その他

Commons IO – Commons IO Overview
だと
http://commons.apache.org/io/api-release/org/apache/commons/io/IOUtils.html#closeQuietly(java.io.Closeable)
というメソッドもある。「例外を握りつぶしたらだめ!」とはよくいわれるけど、finallyで確実にcloseしたいので、ということだろうか。

DbUtils – JDBC Utility Component
にも、おんなじようなメソッドがある。
DbUtils (Apache Commons DbUtils 1.7 API)

JavaServletの機能でも、web.xmlに、
HTTPステータスコードや例外に応じて、ページ遷移する設定を書けたはず。

Struts 1.3だと、まんまExceptionHandlerなんて仕組みがあった。
やっぱり、設定ファイルに、例外ごとにExceptionHandlerを記述する。

最近のはわからない。

DIコンテナのAOPを勉強すると、AOPを活かしたひとつの例として、ロギング、トランザクション管理とともに例外処理ものっている。

Javaの例外テクニックを知る (3/3):EclipseでJavaに強くなる(4) - @IT
これは例外処理のコードなわけか。