##例外とは
プログラム実行中に発生する何らかのトラブルを指す。
- プログラマーの不注意で作りこむバグ
- 要件や仕様の間違い
- 実行マシンの不都合や- ほかのソフトウェアとの連携不都合
など、その種類は多岐に渡るが、ユーザーが利用するソフトウェアは、トラブルが発生したからといって停止してしまったり、動作しなくなったり、動作しても正常に処理をしなかったりしなくてはいけない。
そのためプログラマーは、さまざまな事態に対応する「万一の場合に備えたプログラミングが要求される。
それに対応する役割を担うのがこの例外処理と言える。
##try、catch、finally
例外が発生する可能性がある処理をtryブロックで括り、例外が発生したときの処理をtryブロックに続くcatchブロックに記述する。
####try
tryブロック内には、複数の文を記述することができる。
もし、例外が発生したらtryブロック内の以降の処理はスキップされ、すぐに対応するcatchブロックに制御が移る。
一度catchブロックに制御が移ると、それ以降のtryブロックの処理は必要性に関わらず実行されることがなくなる
####catch
catchブロックは例外が発生したときの処理を記述するためのブロックである。
catchブロックの目的はプログラムを正常な状態に復帰させることで、このブロックの処理が終了すると「トラブルは収束したとして、正常な動作に戻る。
なお、catchブロックは、発生した例外の種類ごとに複数記述できるため、例外の種類ごとに対処処理を変えることができる。
また、catchブロックを省略することも可能。catchブロックを省略するのは、そのメソッド内では、例外の処理方法を決められないケースである。
複数のcatchブロックを用意した際に、どのパターンの例外が発生しても到達できないコードを記述した場合、コンパイラはコンパイルエラーを発生させる。
【到達不可能なケースの例】
public class SampleException extends Exception{}
public class SubSampleException extends SampleException{}
public class Main {
public static void main(String[] args) {
try {
sample();
sub();
} catch (SampleException e) {
System.out.println("A");
} catch (SubSampleException e) {
System.out.println("B");
}
}
private static void sample() throws SampleException {
throw new SampleException();
}
private static void sub() throws SubSampleException{
}
mainメソッドtryブロックの1行目で、sample()メソッドを呼び出し
→catch(SampleException e){}ブロックが受け取り、ブロック内に記述された処理内容を行う。
mainメソッド2行目で、sub()メソッドを呼び出し
→catch(SampleException e){}ブロックが受け取り、ブロック内に記述された処理内容を行う。
SubSampleExceptionに到達するよりも前に、スーパークラスのSampleExceptionでcatchされる。
よって、
【出力結果】
A
A
このような結果になる。
tryブロック二行目でsub()メソッドを呼び出すことによってSubSampleExceptionをスローしているが、SampleExceptionクラスはSubSampleExceptionクラスのスーパークラスであるため、SampleExceptionブロックでもこの例外を受け取ること(catch)ができる。
そのため、SubSampleExceptionで例外を受け取るブロックよりも前に、先に書かれているSampleExceptionクラスで受け取るブロックで処理が行われる。
このような場合、SubSampleExceptionでのcatchブロックは、到達不可能なコードと判断され、コンパイラはコンパイルエラーを発生させる。
####finally
- つないだままのネットワーク切断
- 開いたままになっているファイルのクローズ
- 保持したままのデータベースの接続状態の開放
など、例外発生の有無にかかわらず、実行したい処理はプログラムの形態を問わずに存在する。
こういった例外発生の有無にかかわらず実行したい処理を記述するためのものがfinallyブロックである。
finallyブロックを記述した場合、例外が発生しなければtryブロック実行後にfinallyブロックに記述した処理が実行される。例外が発生した場合は、tryブロックからcatchブロックに制御が移り、その後finallyブロックが実行される。
finallyブロックは、例外発生の有無にかかわらず、実行したい処理を記述するためのものであり、これはcatchブロック内でreturnされていても同じで、 returnによって呼び出し元のメソッドに制御が戻る前に、finallyブロックは必ず実行される。
finallyブロックは、例外の種類によって分ける必要がない。
finallyブロックが実行されないのは、tryブロックやcatchブロック内でSystem.exitメソッドを呼び出して、アプリケーションを強制終了したときか、JVMやOSがクラッシュしたときだけである。
##try-catch-finally構文の記述におけるルールについて
- try-catch-finally文の構文は、その出現順を変更することはできない。
→したがって、catch-try-finallyやtry-finally-catchのように順番を変更することはできない。
- ネストしたtry-cacth-finally文を記述することができる。
public class Main {
public static void main(String[] args) {
try {
try {
String[] array = { "A", "B", "C" };
System.out.println(array[3]); //①
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("D"); //②
} finally {
System.out.println("E"); //③
}
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("F");
} finally {
System.out.println("G"); //④
}
}
}
このように複数のtry-catchがネストしている場合、スローされた例外を受け取るのは、その例外に対応したもっとも近いcatchブロックである。
そのため、上記のコードでは、
① tryブロックで3つの値しか存在しない配列arrayに対して、4つ目の値を出力しようとしたため、cacthブロックに制御が移り、
② ①で発生した例外に対応するもっとも近いcatchブロックで処理を行った後
③ 内側のfinallyブロックで処理を行い、
④ 最後に外側のfinallyブロックが処理を行う。
といった流れになる。
このとき、finallyブロックは必ず実行されるため、内側、外側の両方のfinallyの処理が実行される。
##throwとthrows
Javaの例外処理には「throw」と「throws」という処理がある。
どちらも処理を「投げる」という意味だが、その内容は大きく異なる。
####throw
例外を意図的に起こして、例外処理を行わせる。
メソッド内で記述を行う。
void sample (int num) {
if(num < 0) {
throw new IllegalArgumentException(“負の値は使用できません。”);
}
}
####throws
メソッド内で発生した例外を、自身のメソッド内では処理をせず、メソッドの呼び出し元で例外処理を行う。
throwsが書かれたメソッド内には例外処理の記述をせず、throwsが書かれたメソッドを呼び出した側のメソッドで例外処理を行う。
public static void sample() throws IOException{
FileWriter fw = new FileWriter("data.txt");
}
FileWriterは文字ファイルを書き込むための簡易クラスで、try-catch文でキャッチしないとコンパイルエラーになる検査例外に該当するため、処理を行う際は必ずtry-catch文を記述する必要があるが、throwsの記述を行うことで、メソッドの呼び出し元で対応させている。
##検査例外、非検査例外について
Javaにおけるプログラムの実行中に発生するトラブルには、大きく分けて、2つの種類がある。
- 実行環境のトラブルなど、プログラムからは対処しようのない自体を表すエラー
- プログラムが対処できる例外
の2つである。
例外はさらに、検査例外と非検査例外に分かれる。
- 検査例外 例外処理を記述したかどうかをコンパイラが検査する例外(try-catchが必須)
- 非検査例外 例外処理を記述したかどうかをコンパイラが検査しない例外(try-cacthは任意)
Javaは例外処理をプログラマーが記述し忘れることを防ぐために、検査例外を基本としている。
ソフトウェアが巨大化、複雑化していく歴史の中で、例外処理をプログラマーが記述し忘れる失敗が増え、バグの原因となった反省から、コンパイラによる自動チェック機能が盛り込まれたということが背景にある。
Javaの例外クラスは大きく分けて、
- Error
- Exception
- RuntimeException(Exceptionのサブクラス)
に分かれ、それぞれエラー、検査例外、非検査例外を表している。
Exceptionクラスのサブクラスは、RuntimeExceptionとそのサブクラスを除いて、すべて検査例外である。
そのため、Exceptionクラスを継承している例外クラスは、try-catchしているか、もしくはthrows句で宣言しているかのどちらかを強制される。一方、ExceptionのサブクラスであってもRuntimeExceptionとそのサブクラスは非検査例外として扱われる。(RuntimeExceptionはExceptionのサブクラス)そのため、RuntimeExceptionとそのサブクラスはtry-catchを強制されない。もちろん強制されないだけで、try-catchを記述することはできる。また、非検査例外はthrows句で宣言していなくても、どちらでも問題ない。
エラーはErrorクラスが表すが、実際に使われるのは、そのサブクラスである。
たとえば、Errorを継承したサブクラスには、OutOfMemoryErrorやNoClassDefFoundError、StackOverflowErrorなどがある。なお、エラーに分類されるためには、Errorを継承している必要がある。
これらのトラブルが発生した場合、対応するErrorのサブクラスのインスタンスをJVMが作成し、プログラムに通知する。これは例外と同じメカニズムだが、**エラーの場合は例外と違って「プログラムで対処する」ことを求められていない。**そのため、try-catchしたり、throwsで宣言したりする必要はない。(「求められていない」だけであって、必要であればcatchして処理させることも可能。)
- 各クラスがどのような種類の例外に対応しているか
- 検査例外、非検査例外のどちらに該当するか
などについては
Java SE 8 API仕様
https://docs.oracle.com/javase/jp/8/docs/api/
から詳細について確認ができる。