本章ではプログラム実行時において例外的に発生したエラーを、適切に対処するための例外処理方法について学習します。
本章で学習する内容を動画としてまとめたものです。最初に一通り見終わった後で、学習に入るようにしてください。
プログラミングでは、コンパイルエラーがなく、無事にコンパイルできたとしても、そのプログラムの実行時にエラーが発生することは日常茶飯事です。
こういった実行時エラーはそのまま放っておくと、急にシステムがダウンしたり、意味のわからないエラーメッセージが出てきたりと大変不安定で障害の多いシステムとなってしまいます。
実行時のエラーには、例えば、以下のようなものがあります。
こういったエラーは、構文エラーのようにコンパイルエラーが発生するわけでなく、実行したときに初めて発生するエラーです。
これらのエラーのことを「例外」と呼び、例外が発生したときに適切に処理を行うことを「例外処理」と呼びます。
Javaでは、例外を扱うために例外クラスが用意されており、エラーが発生すると例外クラスからオブジェクトが生成され、そのオブジェクトにエラー情報を持たせます。
例外処理とは、この例外クラスから生成されたオブジェクトの処理方法を言います。
それでは、実行時に発生する例外の種類をまず確認しておきましょう。
プログラム処理例外は、Javaの仕様で対象の例外クラスがあらかじめ定義されています。
処理例外ケースが発生した場合には、その例外クラスを使用してオブジェクトが生成されるようになっています。
それでは、例外クラスの種類を確認しましょう。
例外クラスは、多くの種類がありますが、以下のように分類できます。
全ての例外クラスは、Throwableクラスを継承しています。
Throwableクラスは、以下の2つの子クラスに継承されています。
全てのクラスは、この2つの子クラスのどちらかを継承して定義されていることになります。
Exceptionクラスの子クラスに、RuntimeExceptionクラスというクラスがありますが、このRuntimeExceptionクラスの子クラスとして定義されている例外は、例外処理が必須ではないため、書いても書かなくても良いことになっています。
例えば、0除算のエラーや型変換のエラーなどが該当します。これらは、例外処理を記述しなくもプログラムロジックで回避することが可能ですので、例外処理を記述しなくてもコンパイルエラーにはなりません。
Exceptionクラスの子クラスでRuntimeException以外の子クラスは、全て例外処理は必須となります。
例えば、データベースとの接続やファイルへのデータの書き込みなどは、エラーが発生しやすく、またロジックで回避することは困難であるため、必ず例外処理を記述する必要があります。
例外処理を記述しないとコンパイルエラーになるので、気をつけましょう。その他、例外処理が必須である例外クラスとしては、開発者が独自で定義した独自例外クラスも同様になります。
例外が発生したときのエラー表示について確認してみましょう。
以下は、0除算エラーで発生するArithmeticExceptionという例外のサンプルソースです。
public class MyExceptionSample {
public static void main(String[] args) {
// 0除算のためエラー
int x = 5 / 0;
System.out.println("0除算のためエラー");
}
}
このプログラムを実行すると、以下のようなエラーがコンソールに出力されます。
Exception in thread "main" java.lang.ArithmeticException:/by zero
at MyExceptionSample.main(MyExceptionSample.java:5)
実行結果1行目で、発生した例外が「java.lang.ArithmeticException」であり、「by zero」で0除算によるエラーであることを示しています。
また、実行結果2行目の「MyExceptionSample.java:5」で例外発生した場所がソースの5行目であることが分かります。
例外が発生し、上記のようなエラーがコンソールに出力される場合、プログラムの実行が5行目で中断されているため、その次の「0除算のためエラー」という文字列は出力されません。
このようなエラーメッセージは上述したように、どこで、どのようなエラーが発生しているのかを詳細に示しています。
エラーメッセージをきちんと読み解くことが、バグ改修の精度やスピードの向上に繋がっていきます。
そのため、エラーメッセージは、必ず自身でしっかりと読み解くクセをつけましょう。
それでは、実際に例外処理の方法について学習しましょう。
例外処理については、try・・catchという構文で対応することが基本になります。
try・・catch文の構文は、以下のとおりになります。
例外処理を行いたい例外が発生する可能性がある箇所にtry{}で括って、そのtry{}の下にcatchを記述することで、例外が発生した場合は、catchに制御を移すようにします。
catchのところにエラーメッセージ出力など例外発生時に行いたい処理を記述します。
try {
//例外が発生する可能性のある処理
} catch (例外クラス名 e(例外オブジェクトの参照変数名)) {
// 例外を受け止めるcatchブロック
// try{}の中で発生した例外を該当の例外クラスのオブジェクトeで受け止めます。
// ここに例外発生時に行いたい処理(エラーメッセージ出力など)を記述します。
}
また、以下のようにcatch文を複数記述することも可能です。
try{}の中で複数例外が発生する可能性がある場合、また複数例外の処理を行いたい場合に、以下のように記述します。
各例外の該当例外クラスのところでcatchされます。
このような記述をすると、例外処理内容を各例外に合わせて変更することができます。
try {
// 例外1が発生する可能性のある処理
// 例外2が発生する可能性のある処理
// 例外3が発生する可能性のある処理
} catch (例外1クラス名 e(例外1オブジェクトの参照変数名)) {
// 例外1を受け止めるcatchブロック
// try{}の中で発生した例外1を該当の例外クラスのオブジェクトeで受け止めます。
// ここに例外発生時に行いたい処理(エラーメッセージ出力など)を記述します。
} catch (例外2クラス名 e(例外2オブジェクトの参照変数名)) {
// 例外2を受け止めるcatchブロック
// try{}の中で発生した例外2を該当の例外クラスのオブジェクトeで受け止めます。
// ここに例外発生時に行いたい処理(エラーメッセージ出力など)を記述します。
} catch (例外3クラス名 e(例外3オブジェクトの参照変数名)) {
// 例外3を受け止めるcatchブロック
// try{}の中で発生した例外3を該当の例外クラスのオブジェクトeで受け止めます。
// ここに例外発生時に行いたい処理(エラーメッセージ出力など)を記述します。
}
finallyブロックは、例外が発生するしないに関係無く、常に実行されるブロックです。
try {
// 例外が発生する可能性のある処理
} catch (例外クラス 参照型変数) {
// 例外が発生したときの処理
} finally {
// 例外が発生した場合もしなかった場合も実行する処理
}
それでは、例外処理のサンプルソースを確認してみましょう。
以下は、int型の0除算でArithmeticException例外が発生するサンプルソースです。
public class MyException1 {
public static void main(String[] args) {
int a, b, c;
a = 3;
b = 0;
// 変数aを変数b(値は0)で割ると例外が発生します
c = a / b;
System.out.println("正常終了");
}
}
実行すると、コンソール画面に以下のような例外のメッセージが表示されます。
Exception in thread "main" java.lang.ArithmeticException:/by zero
at MyException1.main(MyException1.java:9)
例外が発生すると、Java仮想マシンは、その例外に対するcatch文を探します。
もし、そのメソッド内に見つからない場合は、そのメソッドを呼び出したメソッドから、catchブロックを探します。
最終的にmainメソッドでも見つからない場合、プログラムは強制的に終了し、画面に例外が発生したことを知らせるメッセージが表示されます。
それでは、上記のプログラム「MyException1」に例外処理を追加してみましょう。以下がサンプルソースです。
public class MyException1 {
public static void main(String[] args) {
int a, b, c;
a = 3;
b = 0;
try {
// 変数aを変数b(値は0)で割ると例外が発生します
c = a / b;
} catch (ArithmeticException e) {
// 例外が発生すると実行します
System.out.println("例外発生");
return;
} finally {
// 例外が発生してもしなくても実行します
System.out.println("finallyです。");
}
// 例外が発生しなければ実行します
System.out.println("正常終了");
}
}
例外処理を正しく行えば、プログラムが強制的に終了することはありません。
実行すると、画面に「例外発生」と表示して、return文により、mainメソッドが終了していることがわかります。
return文でmainメソッドが終了する前に、finally文が実行され、「finallyです。」が表示されます。
例外発生
finallyです。
try・・catch文の使い方は理解できたでしょうか。例外処理は、重要な文法ですので、よく理解するようにしましょう。
throwという構文を使って、意図的に例外を発生させることができます。
投げられた例外は、その例外を受け取れるcatchブロックで処理します。
throw文を利用するとtryの中で強制的にcatchへ制御を移行させることもできます。
以下がthrow文の書式です。
throw new 例外クラスのコンストラクタ();
すでに例外クラスのインスタンスが参照型変数にセットされている場合は、以下の構文になります。
throw 例外クラス型参照変数;
以下がサンプルソースです。実行すると、ArithmeticException例外が発生していることが分かります。
public class MyException2 {
public static void main(String[] args) {
int a = 0;
if (a == 0) {
// 意図的に ArithmeticException 例外を発生させます
throw new ArithmeticException();
}
}
}
Exception in thread "main" java.lang.ArithmeticException
at MyException2.main(MyException2.java:8)
これは例でありArithmeticExceptionはRuntimeExceptionクラスの子クラスなので、try…catchの記述は強制されません。
以下のように、tryブロック内で例外を発生させて、一致する例外クラスが指定されているcatchブロックに制御を移すこともできます。
try {
例外が発生する可能性がある処理1
if (条件式) {
// 条件式がtrueならthrowします
throw new 例外クラス A();
}
// throwされるとcatchへ飛ぶのでここは、実行されません
例外が発生する可能性がある処理2;
} catch (例外クラス A 参照変数) {
例外 A が発生したときの処理;
}
・
・
・
}
Javaの標準例外クラスは数多くあります。APIドキュメントで、JDKパッケージのクラスがどのようなときにどのような例外が発生するのかを調べることができます。
メソッドが投げる例外の種類は、メソッドの宣言を調べ、その例外クラスの詳しい内容は、Exceptionクラスの子クラスを調べます。
もし、目的に合う例外クラスが存在しない場合は、Exceptionクラスを継承したオリジナル例外クラスを定義できます。
例外クラスも、Exceptionクラスを継承している以外は、他のクラスと全く同じです。
必要であれば、コンストラクタやメソッドを追加できます。
以下がサンプルソースです。Exceptionクラスを継承して、MyErrExceptionクラスを定義しています。
また、引数なしのコンストラクタも定義されています。
public class MyErrException extends Exception {
MyErrException() {
System.out.println("オリジナル例外");
}
}
オリジナル例外クラスは、throw文で例外を発生させ、try・・catchの処理ができます。
以下がサンプルソースです。
public class MyException3 {
public static void main(String[] args) {
int a = 0;
if (a == 0) {
// 例外処理は省略できない。
try {
throw new MyErrException();
} catch (MyErrException e) {
System.out.println("MyErrException発生");
}
}
}
}
オリジナル例外
MyErrException発生
オリジナル例外クラスは、RuntimeExceptionクラスの子クラスではないので、例外処理は省略できません。
省略するとコンパイルエラーになりますので、注意しましょう。
定義したメソッドが例外を発生する可能性がある場合、その例外クラスをメソッドの宣言に明示的に記述できます。
メソッドの中でthrowsで明示した例外が発生すると、そのメソッドの処理が終了し、呼び出し元に制御が移ります。
このとき、発生した例外オブジェクトは呼び出し元にそのまま引き渡されます。
よって、throwsを定義したときは例外処理を呼び出し元で行うことができます。
throws文は明示的に宣言することで、メソッドで発生する例外の種類がすぐに分かるため、利用するときに例外処理を適切に行うことができます。
メソッド定義で例外の宣言をする構文は、throws文です。以下が書式になります。
戻り値のデータ型 メソッド名() throws 例外クラス名 {
}
以下がサンプルソースです。myTestメソッドでArithmeticExceptionの例外が発生することを明示しています。
このメソッドを利用するときにこの例外が発生した場合は、ArithmeticExceptionクラスのオブジェクトが引き渡されてきます。
void myTest() throws ArithmeticException {
int a, b, c;
a=3;
b=0;
// ArithmeticException例外が発生
c = a / b;
}
複数の例外を返す可能性がある場合は、throwsの後ろの例外クラスをカンマで区切って記述していきます。
void myTest() throws ArithmeticException, NumberFormatException {
int a, b, c, d;
a=3;
b=0;
// NumberFormatException例外の発生の可能性あり
d = Integer.parseInt(args[0]);
// ArithmeticException例外が発生
c = a / b * d;
}
ArithmeticExceptionクラスやNumberFormatExceptionクラスは、RuntimeExceptionの子クラスなので、サンプルのmyTestメソッドの呼び出すときに例外処理は強制されません。
しかし、RuntimeException以外の例外クラスをthrows宣言しているメソッドを呼び出す場合は、例外処理を実装しないとコンパイルエラーになりますので、注意しましょう。
以下のサンプルは、myErrTestメソッドがオリジナルMyErrException例外を発生する可能性があることを宣言しています。
実行すると、myErrTestメソッドが呼ばれます。このmyErrTestでオリジナル例外を発生させて、呼び出し元で例外処理を行っています。
public class MyException4 {
public static void main(String[] args) {
int a = 0;
if (a == 0) {
// 例外処理は省略できません
try {
// MyErrException 例外が発生する可能性あり
myErrTest();
} catch (MyErrException e) {
System.out.println("MyErrException発生");
}
}
}
static void myErrTest() throws MyErrException {
throw new MyErrException();
}
}
オリジナル例外
MyErrException発生
このように、throws文は例外が発生したところで例外処理をせずにメソッドの呼び出し元に例外処理を任せてしまう方法です。
throws文を使うことで、呼び出し元でどのような例外処理を行うのかを決めることができるようになります。
これにより、呼び出し元のクラスで様々なクラスを呼び出す場合に、例外処理を共通化したり、呼び出したクラスで発生し得る例外の処理の漏れを未然に防ぐことも可能になります。
また、throws文は、throw文と単語は似ていますが、意図的に例外を発生させるthrow文とは全く役割が異なりますので、混同しないようにしましょう。
try・・catch文のcatchの中に記述される例外発生時の処理内容(エラーメッセージ出力、エラーログ書き出し、エラーメッセージ送信など)を「例外ハンドラ」と呼びます。
例外が発生すると、該当する例外クラスが記述されているcatch文を検索します。
例えば、ArithmeticException例外が発生した場合は、以下のようなcatch文でキャッチすることができます。
} catch (ArithmeticException e) {
// 例外ハンドラの記述
}
また、Exceptionクラスは、ArithmeticExceptionの親クラスにあたりますので、ArithmeticExceptionの例外が発生した場合は、Exceptionクラスでもキャッチできます。
} catch (Exception e) {
// 例外ハンドラの記述
}
複数のcatch文で記述する場合、先にExceptionクラスのcatch文を記述してはいけません。
Exceptionクラスが全ての例外がキャッチされてしまうため、それ以下のcatch文で処理することができずコンパイルエラーになりますので、注意しましょう。
// 全ての例外をキャッチして、例外処理
} catch (Exception e) {
// Exceptionでキャッチされ実行されないため、コンパイルエラー
} catch (ArithmeticException e) {
}
よって、複数のcatch文を記述する場合には、以下のように親クラスの記述は、後にする必要があります。
// ArithmeticExceptionの例外処理
} catch (ArithmeticException e) {
// ArithmeticException以外のExceptionの例外処理
} catch (Exception e) {
}
throws文を明示的に記述せずメソッド内で例外が発生し、catchされていない場合、呼び出し側に例外ハンドラを探しに行きます。
以下がサンプルソースです。
public class MyException5 {
public static void main(String[] args) {
// 例外がcatchされない場合、プログラムは強制終了し、例外が画面に表示されます
// myTest1メソッドを呼び出します
myTest1();
}
static void myTest1() {
// myTest1メソッドではmyTest2メソッドを呼び出します
myTest2();
}
static void myTest2() {
// myTest2メソッドではmyTest3メソッドを呼び出します
myTest3();
}
static void myTest3() {
int a, b, c;
a = 3;
b = 0;
// 例外は発生したメソッド内で catch しないと、呼び出した側へ渡されます
// myTest3メソッドでは例外が発生します
c = a / b;
}
}
Exception in thread "main" java.lang.ArithmeticExcetpin: / by zero
at MyException5.myTest3(MyException5.java:26)
at MyException5.myTest2(MyException5.java:16)
at MyException5.myTest1(MyException5.java:11)
at MyException5.main(MyException5.java:6)
発生した例外は、Java仮想マシンに渡る前に、どこかでcatchすればよく、最悪mainメソッド内でもOKとなります。
以下のサンプルは、上記「MyException5」クラスのmainメソッドにcatch処理を追加した例です。
public class MyException5 {
public static void main(String[] args) {
try {
// myTest3メソッドの例外がここまでスローされて
myTest1();
// ここで例外をcatchします
} catch (ArithmeticException e) {
System.out.println("例外発生");
return;
}
System.out.println("正常終了");
}
static void myTest1() {
// myTest1メソッドでは例外をcatchしません
myTest2();
}
static void myTest2() {
// myTest2メソッドでは例外をcatchしません
myTest3();
}
static void myTest3() {
int a, b, c;
a = 3;
b = 0;
// 例外が発生しますが、catchしません
c = a / b;
}
}
例外は、mainメソッドでcatchされ、画面には「例外発生」と表示しています。
例外発生
このように、throws文がなくても、例外は呼び出し元に渡されていきますが、throws文で説明したように、メソッドにthrowsの明示がない場合、どんな例外が発生するかがわかりにくくなります。
そのため、利用する側で例外処理が抜けてしまうことがありますので、例外を呼び出し側で処理してほしい意図がある場合は、throws文を使用するようにしましょう。
以下のプログラムは、IntegerクラスのparseIntメソッドで文字を数字に変換していますが、変換できない場合にNumberFormatException例外が発生します。
数字以外の文字列が引き渡された場合には、画面に「数字を入力してください」と表示するように例外処理を追加してください。
※ コマンドライン引数で値を引き渡して実行しましょう。>
public class ExceptionMondai1 {
public static void main(String[] args) {
// コマンドライン引数で引き渡された値をint型に変換
int num = Integer.parseInt(args[0]);
// 10倍に計算
int kekka = num * 10;
// 10倍した結果を出力
System.out.println(num + "の10倍は、" + kekka + "です。");
}
}
public class ExceptionMondai1 {
public static void main(String[] args) {
// 例外が発生する可能性があるため、例外処理
try {
// コマンドライン引数で引き渡された値をint型に変換
int num = Integer.parseInt(args[0]);
// 10倍計算
int kekka = num * 10;
// 10倍した結果を出力
System.out.println(num + "の10倍は、" + kekka + "です。");
} catch (NumberFormatException e) {
// 例外発生時のメッセージ出力
System.out.println("数字を入力してください。");
}
}
}
コマンドライン引数で引き渡された値を「args[0]」で取得し、その値をIntegerクラスのparseIntメソッドでint型に変換しています。
この処理で、コマンドライン引数が数字でなかった場合、int型の変換ができずにNumberFormatExceptionという例外が発生します。
まず、最初に例外処理を追記する前に、NumberFormatExceptionが発生することを確認すると良いでしょう。
例外が発生する場所を囲むように、try・・catch文を記述します。
catch文では、NumberFormatExceptionでcatchするように定義し、その中の処理で例外メッセージを出力します。
問題1のプログラムにint型に変換した数字が1000以上の場合、オリジナル例外クラスMyErrException1の例外が発生するようにif文とthrow文を追記してください。
また、MyErrException1の例外発生時には、画面に「1000未満の数字を入力してください」と表示するように例外処理も追加してください。
※ コマンドライン引数で値を引き渡して実行しましょう。>
※ 以下の「ExceptionMondai1」クラスのソースには、問題1のプログラムで記述する例外処理は記述しておりません。
あくまで追記位置を示すためのものとなっております。
public class MyErrException1 extends Exception { // オリジナル例外クラスの定義
}
public class ExceptionMondai1 {
public static void main(String[] args) {
// コマンドライン引数で引き渡された値をint型に変換
int num = Integer.parseInt(args[0]);
/* ここにif文とthrow文を追記 */
// 10倍に計算
int kekka = num * 10;
System.out.println(num + "の10倍は、" + kekka + "です。");
}
}
public class ExceptionMondai1 {
public static void main(String[] args) {
// 例外が発生する可能性があるため、例外処理
try {
// コマンドライン引数で引き渡された値をint型に変換
int num = Integer.parseInt(args[0]);
// 引数の値が1000以上の場合
if (num >= 1000) {
// オリジナル例外MyErrException1を発生
throw new MyErrException1();
}
// 10倍計算
int kekka = num * 10;
// 10倍した結果を出力
System.out.println(num + "の10倍は、" + kekka + "です。");
// int型への変換の例外処理
} catch (NumberFormatException e) {
// 例外発生時のメッセージ出力
System.out.println("数字を入力してください。");
// 1000以上のときの例外処理
} catch (MyErrException1 e) {
// 例外発生時のメッセージ出力
System.out.println("1000未満の数字を入力してください。");
}
}
}
コマンドライン引数で引き渡された値をint型に変換した後に、その値が1000以上かをif文で判定します。
1000以上の場合、オリジナルで定義した例外MyErrException1の例外をthrow文で発生させます。
例外が発生すると、それ以降の10倍計算や出力処理は実行されずに、catch文に処理の制御が移ります。
問題1で記述したNumberFormatExceptionの例外処理もありますので、catch文を並べてそれぞれの例外がcatchできるように記述するようにしましょう。
練習問題1のように、コマンドライン引数の値が数字でない場合、「数字を入力してください。」、1000以上の場合、「1000未満の数字を入力してください」と画面に表示するようにプログラムを作成しましょう。
CalcクラスのgetBaisuメソッドに「引数の値が1000以上だったら、MyErrException2例外をthrow文で発生させる」処理を追加してください。
ただし、「ここに追記」以外の個所を修正してはいけません。
public class MyErrException2 extends Exception {
}
public class Calc {
public String getBaisu(String str) /* ここに追記(throws文) */ {
// 引数で渡された値をint型に変換
int num = Integer.parseInt(str);
/* ここに追記(1000以上だったら、MyErrException2例外をthrow文で発生) */
// 引数で渡された値を10倍にして戻す
return String.valueOf(num * 10);
}
}
※この問題の解答は掲載しておりません。Tech Fun ITスクールのJava研修では、講師が丁寧に解説しています。
問題1で作成したCalcクラスのgetBaisuメソッドを呼び出す、「ExceptionMondai2」クラスを作成してください。
なお、getBaisuメソッド呼び出し時に発生する例外については、以下のように処理してください。
・NumberFormatExceptionが発生した場合、「数字を入力してください。」とコンソール出力する。
・MyErrException2が発生した場合、「1000未満の数字を入力してください」とコンソール出力する。
public class ExceptionMondai2 {
// 引数で渡された値を10倍計算するメソッド
public static void main(String[] args) {
String str = "";
Calc calc = new Calc();
// getBaisuメソッドの呼び出し処理
str = args[0] + "の10倍は、" + calc.getBaisu(args[0]) + "です。";
System.out.println(str);
}
}
※この問題の解答は掲載しておりません。Tech Fun ITスクールのJava研修では、講師が丁寧に解説しています。
エラーを適切に扱うことはとても大事なことですので、スムーズに実装できるように何度も練習するとよいでしょう。
例外処理についての説明は、以上です。