例外(1)

例外(Exception)はJavaの重要機能の1つですが、適切な使い方をしなければ大きなバグともなり得ます。
特に初学者にとって例外は解りにくく入門書でも深く触れていない為、よく解らないままにされる傾向にあります。
しかし、業務でJavaを扱うのであれば例外処理は最も重要な機能とも言えます。

正常系処理と異常系処理

アプリケーションを開発していく中で正常系処理が極端に難しい事はほとんどありません。
例えば、WebアプリケーションではHTMLフォームからデータを入力し、入力値をデータベースに登録するような処理はよく見かけます。
この時、常に入力データが妥当でデータベース登録も成功するのであれば、これほど楽な開発はありません。
実際には入力データの妥当性チェック(数値項目に数値以外が入ってないかなど)からデータベースへの登録処理が失敗したケースのハンドリング(処理)など、相当数の異常系処理を考慮する必要があります。
そして、一般的には正常系の処理に比べ数も多く処理も煩雑で難しくなります。
しかし、アプリケーションの品質は異常系処理がどれだけ網羅されているかと言っても過言ではありません(正常系が動くのは当前です)。


かつて、例外処理が発明されていなかった頃は、異常系処理には異常コードを使う手法が一般的でした。
異常コードとは関数(Javaで言う所のメソッド)内で問題が発生した時、戻り値として特殊なコード(異常コード)を返す手法です(例えば戻り値としてint型を定義しておき、問題が発生した場合には-1を返す)。


しかし、この戻り値による処理は様々な問題を含みます。
本来は戻り値が必要のない関数(メソッド)でも異常コードを返す為にint型などを戻り値として指定する必要があることです。
また、戻り値に特殊なコードを割り当てられないケースがあります。
例えばcalc関数(メソッド)のint型の戻り値に異常コードを-1としてしまえば、計算結果として-1を返す場合と区別がつきません。
さらに関数(メソッド)を呼び出した箇所では常に異常コードのチェックを記述する必要があります。
そして、異常値であった場合はさらに自分を呼び出している箇所に異常コードを伝播しなくてはなりません。
関数(メソッド)の階層が深くなっていくと、異なる異常コード体系の関数を使わなくてはならないかもしれません。


このように異常コードによる異常系処理は様々な問題を抱えていました。
そこで発明されたのが例外と呼ばれる機能(概念)です。

ゼロ除算

プログラムでエラー処理を考える時の定番はゼロ除算です。
我々は割り算を行う時にゼロで割り算してはいけない(できない)事を常識として知っていると思います。
しかし、次のようなメソッドを定義した場合、メソッドを実行する側で割る数として必ずゼロを代入しないという保障はありません。

    // a を b で割った結果(整数値)を返す
    public int divide(int a, int b)
    {
        int result = a / b;
        return result;
    }

bに0が指定された場合、プログラミング言語によっては予期せぬ値となるかもしれませんし、プログラム自体が終了してしまうかもしれません。
したがって、異常系処理(bに0を指定)を実装する必要があります。
エラーコードとして-1を返すわけにもいかないので、ここでは0で返す事で妥協しましょう。

    // a を b で割った結果(整数値)を返す
    // ただし、b = 0の時は0を返す。
    public int divide(int a, int b)
    {
        if(b == 0)
        {
            return 0;
        }
        int result = a / b;
        return result;
    }

ところで、呼び出し側のコードはもあるメソッドに記述されているとします(メソッドの内容には深い意味はありません)。

    public String foo(int a, int b, int c)
    {
        int x = divide(a, b);
        int y = divide(b, c);
        int z = x + y;
        String result = "結果: " + z;
        return result;
    }

このメソッドではcalcに対して2回呼び出しを行っており、xとyが0の時が異常コードの可能性があります。
そして、aが0でなくxが0の場合は異常であり・・・、そもそもbが0ならばこのメソッドから異常コードを?と複雑になるでしょう。


例外を使ってdivide()メソッドを書き直すと次のように記述する事ができます。

    // a を b で割った結果(整数値)を返す
    // ただし、b = 0の時は例外IllegalArgumentExceptionがthrowされる
    public int divide(int a, int b) throws IllegalArgumentException
    {
        if(b == 0)
        {
            throw new IllegalArgumentException("b == 0.");
        }
        int result = a / b;
        return result;
    }

IllegalArgumentException

java.lang.IllegalArgumentExceptionはクラス・ライブラリで定義された例外(Exception)の1つで、不正な(Illegal)引数(Argument)をメソッドに渡したことを示す例外(Exception)です。
Javaでは例外を「java.lang.Throwableクラスのサブクラス(派生クラス)」として定義しており、IllegalArgumentExceptionクラスもjava.lang.Throwableのサブクラスとなっています。
当然ながらIllegalArgumentExceptionもクラスであり、newすることでインスタンスが生成され、コンストラクタでは文字列として詳細メッセージを渡すことができます。


ところが、このIllegalArgumentException(例外)はreturnされていません。
そもそもreturnしようとしても戻り値と型が合いませんのでコンパイルエラーとなるでしょう。

throw

例外はthrowキーワードを使い呼び出し元に伝播させ、これを「例外を投げる/throwする/スローする」と言います。
throwの後には例外クラス(Throwableのサブクラス)のインスタンスまたは変数を指定します。
このthrow句が実行されると、後続の処理は全て無視されメソッドの呼び出し元に発生した例外をthrowします。

throws

メソッドの中で例外がthrowされる可能性があるならば、メソッドの宣言でthrowされる可能性のある例外を宣言しなくてはなりません。
この例外の宣言はthrowsキーワードを使い、メソッド宣言の最後(引数の後)に行います(抽象メソッドでも同様)。

    戻り値型 メソッド名(引数) throws 例外クラス型, …

例外クラス型は1つでも構いませんし、,(カンマ)区切りで複数の例外クラスを記述することも可能です。
このthrowsによる例外の宣言を行うことで、メソッドで発生する可能性のある例外を呼び出し元が知ることができます。

例外の伝播

例外がthrowされると上位(呼び出し元)のメソッドでは、さらに上位のメソッドへと例外をthrowします。
そして、例外は次々と上位メソッドに伝播され、最終的にはmain()メソッドまで辿りつき、main()メソッドからさらに上位にthrowされます。
つまり、例外は最終的に最上位であるmainメソッドを実行しているJava仮想マシンJVM)まで伝播され、実行を強制終了させます。


したがって、例外が宣言されたメソッドを使用する場合、そのメソッドもまた同じ例外を宣言しなければなりません。

    public String foo(int a, int b, int c) throws IllegalArgumentException
    {
        int x = divide(a, b);
        int y = divide(b, c);
        int z = x + y;
        String result = "結果: " + z;
        return result;
    }

しかし、このように全ての例外を記述していったとすれば、上位のメソッドのthrows句は例外だらけになってしまうでしょう。

例外の種類

Javaの例外は全てjava.lang.Throwableクラスのサブクラス(派生クラス)として定義されますが、Throwableクラスを直接継承するクラスはjava.lang.Errorクラスとjava.lang.Exceptionクラスの2つしかありません。
一般的な例外はErrorクラスまたはExceptionクラスのサブクラスとして定義されます。

Error

java.lang.Errorクラスとそのサブクラス郡は、例外の中でもエラーと呼ばれる特殊な例外です。
エラーはシステムがプログラム的な要因ではなく、システム的な異常が起きた場合にJVMがthrowします。
代表的なエラーはJVMがメモリを使い果たしてしまった場合にthrowされるjava.lang.OutOfMemoryです。


当然、どのようなタイミングでOutOfMemoryが発生するかは原則として解りません。
しかし、どのようなタイミングでもOutOfMemoryは発生する可能性があります。
このようにシステム的な例外は特殊であり、全てをthrows宣言で定義することは煩雑となります。
したがって、JavaではErrorとそのサブクラスに関してはthrowsでの宣言を省略できます

Exception

java.lang.Exceptionクラスとそのサブクラス郡は、狭義の意味で例外と呼ばれます(広義にはErrorを含む)。
例外はプログラムの中で、条件を満たした場合に明示的にthrowされます。
これはクラスライブラリを利用している場合も同様で、ArrayListなどを使用した場合でも不正な値を引数として与えればIllegalArgumentExceptionがthrowされる可能性があります。

RuntimeException(実行時例外)

Exceptionのサブクラスの1つにjava.lang.RuntimeException(実行時例外)クラスがあります。
Errorと同じようにこのRuntimeExceptionとそのサブクラスはthrows宣言を省略できます


実はIllegalArgumentExceptionはRuntimeExceptionのサブクラスである為、throws宣言を行う必要はありません。
throws宣言を行わなくてはならないのはRuntimeExceptionを継承していないExceptionのサブクラスという事になります。