プログラムのある部分で異常状態が発生したとします。その場合、以下のどちらのほうが良いだろう。
前者の場合、プログラムは強制終了しないけれども、場合によってはバグの温床になります。バグの原因となってしまった場合には、原因の究明に恐ろしく時間がかかることがあります。
後者の場合、例外を投げてプログラムを強制終了します。必要に応じてその場で例外が発生しないようにデバッグ作業をすることができます。また、例外は捕捉可能なので、try catch構文を利用して、発生した例外に応じた処理をすることも可能です。
という訳で、基本的には、「異常な状態が発生したら例外を投げる」という形にしたほうが将来に禍根を残さない、ということになります。本節では、例外を投げるプログラムについて解説していきます。
以前、階乗を求めるプログラムを作成しました。当時は、引数に 0以下の数が入る異常状態を想定していませんでした。今回は、引数に 0以下の値が設定されたときに、異常処理をしたいと思います。まず、例外を投げないプログラムの場合、例えば、以下のようになります。
ソースコードは以下の通り。
O501/O501.java
/** * 階乗を求めます(例外を投げない場合)。 */ public class O501 { /** * メインメソッド。 * @param args 引数 */ public static void main(String[] args) { System.out.println("-3の階乗を求めます。"); long value = O501.factorial(-3); if(value == -1L) { System.out.println("異常が発生しました。"); } else { System.out.println("値は " + value + "です。"); } } /** * 指定した数の階乗を求めます。なお、0以下の値を指定した場合、-1を返します。 * @param number 数 * @return 階乗の値 */ private static long factorial(int number) { if(number <= 0) { return -1L; } else { long value = 1; for(int i = number; i >=1 ; i --) { value *= i; } return value; } } }
実行結果は以下の通り。
-3の階乗を求めます。 異常が発生しました。
階乗の演算結果は必ずプラスの値になるので、異常状態の場合には「-1」を返すこととして、区別しています。しかし、この方法には 2つの問題点があります。
前者も後者も問題なのですが、今回の場合は特に後者が問題になります。「階乗は必ず正の値になるから -1が返される時点でそもそもおかしい」ということを知らなければ、-1を正しい計算結果だと思い込んでしまう可能性があります。つまり、将来に禍根を残します。
階乗を求めるメソッドを改良して、例外を投げることとします。try catch構文を使用しない場合について動作確認してみます。
ソースコードは以下の通り。
O502/O502.java
/** * 階乗を求めます(例外を投げる場合)。 */ public class O502 { /** * メインメソッド。 * @param args 引数 */ public static void main(String[] args) { System.out.println("-3の階乗を求めます。"); long value = O502.factorial(-3); System.out.println("値は " + value + "です。"); } /** * 指定した数の階乗を求めます。なお、0以下の値を指定した場合、例外を発生します。 * @param number 数 * @return 階乗の値 */ private static long factorial(int number) { if(number <= 0) { throw new IllegalArgumentException(); } else { long value = 1; for(int i = number; i >=1 ; i --) { value *= i; } return value; } } }
実行結果は以下の通り。
-3の階乗を求めます。 Exception in thread "main" java.lang.IllegalArgumentException at O502.factorial(O502.java:24) at O502.main(O502.java:13)
予約語throwを用いることで、例外 IllegalArgumentExceptionが発生します。try catch構文が使用されていないので、プログラムは強制終了します。そのため、このメソッドの利用者は、異常状態が発生したことを理解することができます。
引数の値を見直すか、あるいは try catch構文を利用するか迫られることになります。続いて try catch構文を使用してみます。
ソースコードは以下の通り。
O503/O503.java
/** * 階乗を求めます(例外を受け取る)。 */ public class O503 { /** * メインメソッド。 * @param args 引数 */ public static void main(String[] args) { System.out.println("-3の階乗を求めます。"); try { long value = O503.factorial(-3); System.out.println("値は " + value + "です。"); } catch(IllegalArgumentException e) { System.out.println("異常が発生しました。"); } } /** * 指定した数の階乗を求めます。なお、0以下の値を指定した場合、例外を発生します。 * @param number 数 * @return 階乗の値 */ private static long factorial(int number) { if(number <= 0) { throw new IllegalArgumentException(); } else { long value = 1; for(int i = number; i >=1 ; i --) { value *= i; } return value; } } }
実行結果は以下の通り。
-3の階乗を求めます。 異常が発生しました。
上手く捕捉することができました。
どの例外を投げるかは重要になります。Exceptionクラスを継承したクラスの中で、発生する異常状態の内容にふさわしい名前の例外を探し出す必要があります。
Exceptionクラスとそのサブクラスである RuntimeExceptionクラスについて、Java SE 7(Java 1.7)の説明書(API)を掲示しておきます→Exceptionクラス・RuntimeExceptionクラス。
もし、自分の納得する例外が見つからない場合には、独自の例外クラスを作成することもできます。詳細については、次の節で解説します。
ランタイム系例外を投げるか、非ランタイム系例外を投げるかによって、そのメソッドを利用する側にかかる手間は異なります。
ランタイム系例外の場合には、利用側は特段のコードを記述しなくても利用できる代わりに、プログラムが強制終了するリスクを負うことになります。逆に、非ランタイム系例外の場合には、利用側は try catch構文で例外を捕捉するか、あるいは throwsを記述して、例外を処理しないことを明示するか、どちらかを選択しなければなりません。
自分が投げる例外は、ランタイム系例外のほうが良いのか、非ランタイム系例外が良いのかについては、時と場合によります。ただ、発生した異常状態の内容にふさわしい名前の例外が見つかったならば、それを投げておけば大丈夫です。その例外が、ランタイム系例外ならそれで良し、非ランタイム系例外ならまたそれで良し、と対応するのが一番だと思います。