例外処理とは
例外とは、プログラム実行中に発生する予期せぬエラーのことです。
このエラーが発生することを「例外がスローされる」といいます。
スローとは英語でthrowと書きますが、まさにプログラムがエラーを「投げる」という意味です。
プログラミングする際、エラーが発生しないように注意深くプログラミングしなければなりません。
エラーが発生するとアプリケーションは強制終了してしまうからです。
しかし防ぎようがないエラーもあります。
一例ですがそれは物理的な問題などです。
ネットワークを利用したアプリケーションを動作させた際、ネットワークが物理的に繋がっていなかったり、ファイルへアクセスするアプリケーションを実行した際、ファイルがユーザによって移動や削除がされており、アクセスした場所にファイル無かった場合などです。
このような問題はプログラムによって防ぐ事が出来ません。
そこで、Javaではエラーが発生した場合でもアプリケーションが強制終了されないようにする方法があります。
エラーが発生した場合、発生したまま放置するのではなく、エラー発生時そのエラーを処理するプログラムを記述する事で、アプリケーションが強制終了される事を防ぎます。
Javaではエラーの事を例外と言い、エラーを処理する事を「例外処理」といいます。
例外処理はなぜ必要か
- 例外が発生した場合(プログラム実行中にエラーが発生した)、何も記述しないとそこで強制終了する。
- 例外が発生した場合、エラーメッセージなどを表示するなどして、引き続きプログラムを実行できるようにする。
【例外の発生例】
public class ErrorSample { public static void main(String[] args) { String array[] = {"Apple","Orange","Lemon"}; for(int i = 0; i < 4 ; i++){ System.out.println(array[i]); } System.out.println("プログラムが最後まで実行されました"); } }
実行結果
Apple Orange Lemon Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 3 at ErrorSample.main(ErrorSample.java: 5)
3行目のString型の配列arrayは、インデックス番号0から2までに、3つの要素が格納されています。
4行目のfor文で配列の各要素にアクセスをしていますが、条件式にi<4
と記述されているので、インデックス番号3までアクセスすることになります。
ところがインデックス番号3は存在しないため、ここでエラーが発生します。
実行結果を見ると、Apple、Orange、Lemonまでは正常に出力をしていますが、次のインデックス番号3にアクセスしたところで、「java.lang.ArrayIndexOutOfBoundsException」という例外が発生して、プログラムを終了しています。
このようにプログラム作成時にはいくつ値が入るかわからないが、実際に実行してみると、値がなくてエラーになったというような場合、JavaVM(Java実行環境)がこれ以上、プログラム実行を続けられないというエラーを発生させて、プログラムを終了させます。
例外処理を行う方法
例外処理を行うには、前節のようにJavaVMがエラーを感知して自動的に実行するものと、プログラマがエラーを想定してプログラムに例外処理を記述する方法があります。
またプログラマが例外処理を行うには次の2つの方法があります。
1. try-catch-finally構文で例外処理をする方法
2. throwsキーワードを使用して例外処理をする方法
まずtry-catch-finally構文を使用する方法から見ていきましょう。
try { 例外が発生するかもしれない処理 } catch (例外クラス型のオブジェクトの宣言) { 例外が発生したときの処理 } finally { 最後に必ず行いたい処理 }
tryブロック(tryからcatchの間、もしくはfinallyまでの間)には、エラーが発生する可能性がある処理を記述します。
catchブロック(catchからfinallyの間、もしくは次のcatchまでの間)には、例外が発生した際に行いたい処理を記述します。
finallyブロック(finallyからブロックの終わりまで)には、例外が発生しても、しなくても必ず実行したい処理を記述します。
上記の構文ではcatchとfinallyを1つずつ記述していますが、必ず1つずつ記述しなければいけないわけではありません。Javaの例外処理では以下のルールがあります。
- 例外処理を行う際、tryブロックは必ず必要。
- catchブロックかfinallyブロックのどちらか1つがあればよい。
- catchブロックは複数記述しても良い。
「例外が発生する」とは、実は例外クラスのオブジェクトが生成された事を言います(正しくは「例外クラスのオブジェクトが投げられた」という表現を用います。投げられるについては後ほど解説します)。
Javaのクラスライブラリには例外を表すクラスが定義されています。
これを例外クラスと言い、例外クラスは1つだけではなく様々な種類の例外を表すため、多くの種類の例外クラスが定義されています。
この例外クラスは継承関係によって作成されており、全ての例外クラスはExceptionクラスをスーパークラスとして作成されています。
そこでcatchブロックの後にある( )の中には例外クラスのオブジェクトを宣言します。
例外クラスのオブジェクトを宣言することにより処理手順は以下の通りになります。
ポイント!tryブロックでエラーが発生した場合
即座にcatchブロックに処理が移動します。
エラーが発生した行よりcatchまでに記述されている処理は無視されます。catchブロックで宣言されている例外クラスのオブジェクトに、発生した例外を参照できるかどうかチェックします。
参照できた場合はそのcatchブロックが処理されます。
参照出来なかった場合は、処理がfinallyブロックに移動するか、まだ次にcatchブロックがあれば同様に例外を参照できるかチェックします。複数のcatchブロックが記述されていても処理されるのは1つのcatchブロックだけです。
catchブロックの処理が終わった場合、finallyブロックへ処理が移行します。
finallyブロックが無い場合は、例外処理はそこで終わりです。finallyブロックが記述されていた場合、エラーが発生する、しないに関わらず必ずfinallyブロックの処理が実行され例外処理を終了します。
ポイント!tryブロックでエラーが発生しなかった場合
tryブロックの処理を記述どおり実行し、catchブロック内は無視され実行されません。
finallyブロックが記述されていた場合は、必ず実行し例外処理を終了します。
記述されていなかった場合は、tryブロックを実行した時点で例外処理は終了です。
【try-catch-finally構文を使った例外処理】
public class ExceptionSample01{ public static void main(String[ ] args){ int a[ ] = {1, 2, 3}; for(int i = 0; i < 5; i++){ try{ System.out.println("配列の要素にアクセスします : " + i); System.out.println(a[i]); } catch(Exception e){ System.out.println("例外が発生しました"); System.out.println(e); } finally { System.out.println("処理が終了しました\n"); } } } }
実行結果
配列の要素にアクセスします : 0 1 処理が終了しました 配列の要素にアクセスします : 1 2 処理が終了しました 配列の要素にアクセスします : 2 3 処理が終了しました 配列の要素にアクセスします : 3 例外が発生しました java.lang.ArrayIndexOutOfBoundsException: 3 処理が終了しました 配列の要素にアクセスします : 4 例外が発生しました java.lang.ArrayIndexOutOfBoundsException: 4 処理が終了しました
ここでは大きさが3の配列を作っています。
インデックス番号は0から2で、3番目以上の要素はありません。
しかし、a[i]のiはint型の変数なので、3以上の値を入れることはできます。
コンパイルには何の支障もないのですが、実行すると存在しないデータにアクセスしようとしてしまいます。
このように「コンパイルできるが正常に実行できない」状態は例外として扱われます。
どんな理由でエラーになったのかという情報は9行目のcatchブロック内に記述されているExceptionクラスの(例外のスーパークラス)のオブジェクトが格納されている変数eに記録されます。
11行目でSystem.out.println(e)
と記述し、変数eを出力処理していますが、ここでは配列の要素数を超えてアクセスした場合に発生するExceptionクラスを継承したArrayIndexOutOfBoundsExceptionという例外クラスのオブジェクトが渡されていることがわかります。
throwsキーワードを使用する方法
次にthrowsキーワードを使って例外処理をする方法を見ていきましょう。
例外が発生する可能性があるメソッドを定義する時に、throwsキーワードに続けて、例外クラス名を記述します。
ここで例外処理を行うのではなく、例外が発生した場合は、そのメソッドの呼び出し元に例外オブジェクトが転送され、呼び出し元で例外処理が行われます。構文は以下の通りです。
[修飾子] 戻り値の型 メソッド名(引数リスト) throws 例外クラス名{ メソッドの処理 }
発生する可能性のある例外クラスが複数ある場合は、カンマで区切って複数指定をします。
[修飾子] 戻り値の型 メソッド名(引数リスト) throws 例外クラス名, 例外クラス名…{ メソッドの処理 }
またメソッドの呼び出し元の方で複数の例外処理をまとめて行う事が可能です。
例えば、try-catchで例外処理を行う場合、メソッドAとメソッドBが同じ例外処理を行うとしても、それぞれ例外処理を記述しなければなりません。
これに対して、throwsキーワードを使うと、メソッドの定義時に、発生する可能性のある例外クラスを指定しておけば、呼び出し元の方で例外処理を受けとり、処理をします。
呼び出し元では、それぞれのメソッドに共通する例外処理を1つだけ記述しておけばよいことになります。
【throwsキーワードを使った例外処理】
public class ExceptionSample02 { static void foo() throws ArrayIndexOutOfBoundsException{ String array[] = {"Apple","Orange","Lemon"}; for(int i=0; i<4; i++){ System.out.println(array[i]); } } public static void main(String[] args) { try{ foo(); }catch(ArrayIndexOutOfBoundsException e){ System.out.println("例外発生!"); } System.out.println("プログラムが最後まで実行されました"); } }
【実行結果】
Apple Orange Lemon 例外発生! プログラムが最後まで実行されました
foo()メソッドでは、3つの要素が格納されたarrayという配列が用意されており、各要素にアクセスをし、それぞれの値を画面表示するプログラムです。
foo()メソッドの定義にthrowsキーワードを付けて、foo()メソッド内で例外が発生した場合は、ArrayIndexOutOfBoundsクラスのオブジェクトを呼び出し元に転送するようにしておきます。
存在しない4つ目の要素にアクセスしようとした時に例外が発生しますが、foo()メソッド内では処理されず、呼び出し元のmain()メソッドへArrayIndexOutOfBoundsクラスの例外オブジェクトが転送されます。
main()メソッド内ではtry-catchを使用して例外処理を記述しており、foo()メソッドで発生した例外をcatchブロックで受け取ります。
catchブロックで例外処理をした後は、プログラムが続行され、正常に最後まで実行されます。
このようにthrowsキーワードを使用した例外処理は例外が発生したメソッドでは例外処理を行わず、そのメソッドの呼び出し元で例外処理を行います。
例外の種類
Javaには多くの例外クラスが用意されています。
例外クラスは大きく3つに分類されています。
ここではJavaに用意されている例外クラスとその特徴について見ていきましょう。
例外クラスはすべてjava.langパッケージのThrowableクラスを継承しています。
Throwableクラスを継承したクラスのオブジェクトは例外が発生したときに自動的に作成されます。
例外オブジェクトが作られることを「例外がスローされる」 「例外が投げられる」といいます。
例外クラスはErrorクラスを継承するクラスとExceptionクラスを継承するクラスに分けられます。
Errorクラスとそのサブクラス
プログラムの実行中に致命的なエラーが発生したときに投げられる例外で、JavaVMで検出される
(例)メモリ不足、スタックオーバーフロー(メモリからデータがあふれだすこと)などExceptionクラスのRuntimeExceptionクラスとそのサブクラス
おもにプログラミングのバグによって発生する例外で、JavaVMで検出される
(例)ArrayIndexOutObBoundsException(配列の要素数を超えてアクセスした場合に発生する例外)
ArithmeticException(整数をゼロで割った場合に発生する例外)などExceptionクラスのRuntimeExceptionクラス以外のサブクラス
プログラミングのよって回避できないエラーが生じた場合に発生する例外
(例)ファイルが存在しない、ネットワーク上でデータ送信ができないなど
これらの例外クラスは、チェック例外と非チェック例外に分けられます。
チェック例外
コンパイル時に、例外処理がなされているかを必ずチェックされる例外。
Exceptionクラスを継承する例外(前図3)はチェック例外で、例外処理をしていない場合は、コンパイルエラーになる。
非チェック例外
プログラミングによって回避できないエラーが生じた場合に発生する例外で、コンパイル時に、例外処理がなされているかをチェックしない。
例外処理をしても、していなくてもコンパイルエラーにならない。
Errorクラスのサブクラス(前図1)とRuntimeExceptionのサブクラス(前図2)がこれにあたる。
次の図は、各カテゴリの主な例外クラスです。
■主な例外クラス
例えばErrorクラスのサブクラスであるNoClassDefFoundErrorは、ファイルを読みこむプログラムを記述したとして、そのコンパイル時には読み込みたいファイルが存在するかどうかはチェックされませんが(非チェック例外)実際に読み込もうとしたときに該当のファイルが見つからなくて発生するものです。
【非チェック例外】
public class ExceptionSample03 { public static void main(String[] args) { int num = Integer.parseInt("ABC"); System.out.println(num); } }
実行結果
Exception in thread "main" java.lang.NumberFormatException: For input string: "ABC" at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) at java.lang.Integer.parseInt(Integer.java:580) at java.lang.Integer.parseInt(Integer.java:615) at ExceptionSample03.main(ExceptionSample03.java:3)
3行目のInteger.parseInt("ABC")の記述で、数値に変換できない文字列をparseInt()メソッドの引数に指定しているためNumberFormatException例外が発生します。
NumberFormatExceptionはRuntimeExceptionクラスのサブクラスで非チェック例外であるため、例外処理をプログラム上に記述しなくてもコンパイルエラーになっていません。
実際に実行してはじめてNumberFormatException例外が発生していることがわかります。
【チェック例外】
import java.io.FileWriter; public class ExceptionSample04 { public static void main(String[] args) { FileWriter fw = new FileWriter("sample.txt"); } }
4行目でコンパイルエラーになることを確認しましょう。
例外処理(throws IOException)を追加して、エラー表示が消えていることを確認します。
import java.io.FileWriter; import java.io.IOException; public class ExceptionSample04 { public static void main(String[] args) throws IOException { FileWriter fw = new FileWriter("sample.txt"); } }
4行目ではファイルの入出力を行うためにFileWriterクラスをインスタンス化しています。
FileWriterクラスのコンストラクタは入出力処理に失敗すると、IOException例外を発生させます。
IOExceptionクラスは必ず例外処理を行わなければならないRuntimeExceptionのサブクラス以外の例外クラスです。
例外処理を記述していないためコンパイルエラーとなっています。
Eclipseではコンパイルエラーなどが発生すると、そのソースコードに赤い波線が表示され、対処方法を示してくれます。
今回は、try-catch構文を記述する方法と、スロー宣言(呼び出し元のメソッドにthrows例外クラス名を指定する)の2つの手段があると教えています。
今回は「スロー宣言の追加」を選択します。
このように例外クラスには、例外処理を必ず記述しなければコンパイルエラーになるチェック例外と、例外処理を記述してもしなくてもよい非チェック例があります。
Throwableクラスを継承した例外クラスは大変たくさんありますので、例外処理を使用したいときには、javadocでどの例外クラスを継承しているのか(ExceptionもしくはRuntimeException)を調べることがポイントになります。
「例外処理 Part2」へ続きます。
phoeducation.work
独学で挫折しそうになったら、オンラインプログラミングスクール