新人エンジニア研修で知っておきたい例外の使い方
なぜ、例外の理解が重要なのか、その理由
この記事では、弊社の新人エンジニア研修の参考にJavaを解説します。
前回はインタフェースについて解説しました。今回は例外処理について解説します。例外処理も継承を巧みに使った仕組みの一例ですので復習にもなると思います。
この短い新人エンジニア研修の中でも例外がいくつか出てきました。
3.演算子のところで紹介したArithmeticException(標準API)
6.配列の作成と使用のところで紹介したArrayIndexOutOfBoundsException(標準API)
9.インスタンスの活用のところで紹介したNullPointerException(標準API)
10.継承(拡張)のところで紹介したClassCastException(標準API)
等がありましたね。
例外とは、人間に向けたプログラムからのトラブル報告書です。このままでは処理を継続できないという報告です。
例えばあなたがお使いのゲームアプリが突然意味不明なメッセージを表示して落ちてしまったらどうでしょうか?その上、何時間もかけて蓄積したセーブデータが残っていないとしたら。。。
エラーが発生しても続行するか、重要なデータを保存してから適切に終了するようにプログラミングしないといけません。
そのような処理を例外処理といいます。
例外が存在しなかった時代のエラー処理は大変でした。深い階層で起こったエラーを一生懸命バケツリレーで呼び出し元の処理に伝えていたのです。メソッドが返せる値は一つだけですから、大した情報も伝えられませんでした。エラーが発生した時にだけ投げる例外という仕組みができたおかげで、エラー時の処理が画期的に改善されたのです。
1.try~catch文
例外処理の構文を以下に示します。
<構文>
try {
例外が投げられる可能性のある処理
}
catch(例外の型1 変数名1) {
例外の型1の例外が投げられたときの処理
}
catch(例外の型2 変数名2) {
例外の型2の例外が投げられたときの処理
}
finally {
最後に必ず行う処理
}
では、順番に解説していきます。
以下のExample01はtry~catch文を解説するプログラムです。
package chap13;
public class Example01 {
public static void main(String[] args) {
try {
System.out.println(5 / 0);
} catch (ArithmeticException e) {
System.out.println("例外が発生しました");
System.out.println(e);
}
System.out.println("このプログラムを終了します");
}
}
<実行結果>
例外が発生しました java.lang.ArithmeticException: / by zero このプログラムを終了します |
このように、例外が発生する恐れのある個所をtryブロックで囲みます。英語の【try】には元来、「上手くいくかどうか分からないが試してみる」という意味があります。
catchブロックには例外が発生した時の処理を書きます。例外をキャッチするという文字通りの意味です。
catchの後ろの()の中には、あたかもメソッドの仮引数のように例外の型と変数を書きます。変数名は何でも良いですが、【error】の頭文字eや【exception】のexを使うのが慣例となっています。変数eの中身をコンソール出力すると例外の内容(原因)が分かります。
もし、例外が発生したらcatchブロックの処理が実行されるのですが、そうするとIF文との違いに迷う新人エンジニアの方がいます。一般的なガイドラインとして、プログラムの制御フローの分岐や条件に基づく処理は、if文を使用し、予期しないエラーへの対応やエラーハンドリングは、例外処理を使用することが推奨されます。
また、例外が発生した時の処理といっても何か有効なトラブル対応処理が書けるわけではありません。最も多い処理は人間にトラブルの内容を知らせてログに記録することです。次にありえるのはデータベースやファイルをちゃんと閉じる処理です。
例外処理がなければここでプログラムが中断するところですが、今回は中断することなく最後まで終えることができています。
また、エラー処理が主要なロジックから分離されている点にも注目してください。tryブロックで囲まれた主要なロジックとcatchブロックで囲まれたエラー処理のコードが分離されているため、コードの主要な目的が理解しやすくなっています。
2.finally句の意義
finallyブロックにかかれた処理は例外発生の有無を問わず必ず実行されます。ファイルのクローズやデータベースのクローズに使うというのがfinallyブロックの典型的な利用例です。
以下のExample02はfinally句を説明するプログラムです。
package chap13;
public class Example02 {
public static void main(String[] args) {
try {
System.out.println(Integer.parseInt("2byte"));
System.out.println("例外発生の有無にかかわらず実行したい処理です");
} catch (NumberFormatException e) {
System.out.println(e);
}
System.out.println("このプログラムを終了します");
}
}
<実行結果>
java.lang.NumberFormatException: For input string: "2byte" このプログラムを終了します |
例外が発生すると処理は一気にcatchブロックまで飛んでしまうため、8行目の処理が実行されません。こんな時はどうしたらよいのでしょうか?
例えば以下のように同じ処理を2回記述するコードは冗長です。
package chap13;
public class Example03 {
public static void main(String[] args) {
try {
System.out.println(Integer.parseInt("2byte"));
System.out.println("例外発生の有無にかかわらず実行したい処理です");
} catch (NumberFormatException e) {
System.out.println("例外発生の有無にかかわらず実行したい処理です");
System.out.println(e);
}
System.out.println("このプログラムを終了します");
}
}
以下のようにfinallyブロックを追加すれば繰り返しの記述を避けることができます。
package chap13;
public class Example04 {
public static void main(String[] args) {
try {
System.out.println(Integer.parseInt("2byte"));
} catch (NumberFormatException e) {
System.out.println(e);
} finally {
System.out.println("例外発生の有無にかかわらず実行したい処理です");
}
System.out.println("このプログラムを終了します");
}
}
<実行結果>
java.lang.ArithmeticException: / by zero 例外発生の有無にかかわらず実行したい処理です このプログラムを終了します |
finallyブロックにかかれた処理は例外発生の有無を問わず必ず実行されます。
当社の新人エンジニア研修の範囲でのfinallyの活用場面としては、データベース処理において例外が発生した場合に、データベースとの接続をクローズするという使い方があります。この点は機会があれば「Javaからデータベースのデータを活用する」というテーマでご紹介したいと思います。それまではfinally句の存在意義は理解できないと思います。
3.複数例外のキャッチ
起こりうる複数の例外の場合に分けて、以下のように例外処理を書き分けることができます。
package chap13;
public class Example05 {
public static void main(String[] args) {
try {
int[] numbers = {1, 2, 3};
numbers[3] = 4;
System.out.println(5 / 0);
System.out.println(Integer.parseInt("2byte"));
} catch (ArithmeticException e) {
System.out.println(e);
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println(e);
} catch (NumberFormatException e) {
System.out.println(e);
}
System.out.println("このプログラムを終了します");
}
}
<実行結果>
java.lang.ArrayIndexOutOfBoundsException: 3 このプログラムを終了します |
このようにtry~catch文は最初に発生した例外を捕捉します。
4.try-with-resourcesを使うとリソースを閉じるコードを書かなくて良い
以前、繰り返しのところで以下のように数え上げ回数をキーボード入力するプログラムを作成しました。
この時に、try-with-resourcesという構文を使っていました。try-with-resourcesは、ファイルやデータベース、ネットワーク接続などのシステムリソースを使用する際に非常に有用です。
これらのリソースは、使用後に閉じる必要がありますが、従来のtry-catch-finallyブロックを使用するとコードが冗長になります。try-with-resourcesを使用すると、リソースが自動で閉じられるため、プログラマはリソースを閉じるコードを書く必要がありません。
以下のExample06はtry-with-resourcesを使っているためScannerをcloseしなくて済んでいます
package chap13;
import java.util.Scanner;
public class Example06 {
public static void main(String[] args) {
System.out.println("何回数え上げますか?");
try (Scanner sc = new Scanner(System.in)) {
int count = sc.nextInt();
int i = 0;
while (i < count) {
System.out.println(i);
i++;
}
}
}
}
try-with-resourcesでは、try文の直後に括弧を使用して、クローズ可能なリソースを宣言します。(このリソースはAutoCloseableまたはCloseableインターフェースを実装している必要があります)
tryブロックが終了したら、自動的にclose()メソッドが呼び出され、リソースが安全に閉じられます。
もしも、try-with-resourcesを使わないと以下のようにfinally句が必要になり冗長なコードになります。かなりコードが簡略化されていたのがお分かりになると思います。
package chap13;
import java.util.Scanner;
public class Example07 {
public static void main(String[] args) {
Scanner sc = null;
System.out.println("何回数え上げますか?");
try {
sc = new Scanner(System.in);
int count = sc.nextInt();
int i = 0;
while (i < count) {
System.out.println(i);
i++;
}
} finally {
if (sc != null) {
sc.close();
}
}
}
}
5.例外クラスの体系
例によってIDEを使いArithmeticExceptionクラスから、スーパークラスをさかのぼって行きましょう。
まずはArithmeticExceptionクラスの40行目をみてください。
Java17標準APIpublic class ArithmeticException extends RuntimeException {(中略)
となっていてRuntimeExceptionクラスのサブクラスであることが分かりました。
では次にRuntimeExceptionクラスの43行目を見てスーパークラスを探します。
Java17標準APIpublic class RuntimeException extends Exception {(中略)
となっています。
このExceptionがすべての例外クラスの親玉(?)です。
さらにExceptionクラスの定義を45行目に見ると以下のようにThrowableクラスのサブクラスとして宣言されています。
Java17 標準APIpublic class Exception extends Throwable {(中略)
【Throwable:投げられる】という意味のクラスですね。
このThrowableクラス(標準API)を調べると以下の記述があります。
Throwable
クラスは、Java言語のすべてのエラーと例外のスーパー・クラスです。
そして、Throwableのスーパークラスは例のObjectクラスです。
Throwableの直系の既知のサブクラスについて以下の記述があります。
クラスThrowable
- java.lang.Object
- java.lang.Throwable
- すべての実装されたインタフェース:
- Serializable
Exceptionクラスには兄弟分のErrorクラスがあるということですね。つまり、ここまでを図示すると下図のようになります。
この中でまず抑えるべきはその例外クラスが、RuntimeExceptionクラスのサブクラスであるか、それ以外のExceptionクラスのサブクラスかという区別です。なぜなら、RuntimeExceptionクラスのサブクラスであれば、try~catch文は書かなくても良いことになっているからです。
例えば、これまでも配列を使ったときにtry~catch文を書かなくても良かったですね。もし、例外が起きる可能性のある全てのコードにtry~catch文を強制していたらJavaプログラムは例外処理で埋め尽くされてしまうからです。
try~catch文の記述が任意である例外を非チェック例外(非検査例外)といいます。
※あるいは実行時例外ともいいます。RuntimeExceptionの直訳です。
ちなみに、今までご紹介してきたArithmeticException、ArrayIndexOutOfBoundsException、NullPointerException、ClassCastException、NumberFormatExceptionはすべてRuntimeExceptionクラスのサブクラスであり、非チェック例外です。
なお、Errorクラスが発生する可能性のあるコードもtry~catch文で囲む必要はありません。
Errorクラス(標準API)の説明を見ると
Error
はThrowable
のサブクラスで、通常のアプリケーションであればキャッチすべきではない重大な問題を示します。そうしたエラーの大部分は異常な状態です。
とあります。
例えば、Java仮想マシンが壊れているとかメモリ不足等の事態です。いかんともしがたいわけです。
過去の記事の中では、1回目のところでStackOverflowErrorというのを見ましたがあれは、ErrorクラスのサブクラスのVirtualMachineErrorクラスのサブクラスです。
Exceptionクラスのサブクラスであって、RuntimeExceptionクラスのサブクラスでないクラスは、すべてチェック例外(検査例外)ですのでtry~catch文で囲む必要があります。
チェック例外として当社の研修内容に関連するものとしては、例えばSQLExceptionがあります。
ただ、これはデータベースに関連した例外ですので、そこでお話ししたいと思います。
上記の例外クラスの体系を見て明らかなように以下のサンプルプログラムは意図した通りの処理を行います。
package chap13;
public class Example08 {
public static void main(String[] args) {
try {
int[] numbers = {1, 2, 3};
numbers[3] = 4;
System.out.println(5 / 0);
System.out.println(Integer.parseInt("2byte"));
} catch (Exception e) {
System.out.println(e);
}
System.out.println("このプログラムを終了します");
}
}
<実行結果>
java.lang.ArrayIndexOutOfBoundsException: 3 このプログラムを終了します |
このように全ての例外クラスはそのスーパークラスであるExceptionクラス型で捕捉できるようになっています。ただし、プログラマの意図が不明瞭になりがちですので手抜きで記述することは避けた方が良いでしょう。
※IDEからも複数のキャッチ句に置換しうることを提案されると思います。
Exceptionクラス型は、具体的な例外を全てcatchして、それでも発生するかもしれない例外を補足する時に使います。
では、ここまでは例外をキャッチする側の処理を見てきましたので、逆に例外をスローする側の処理がどうなっているのかを探検してみましょう。
6.throwsキーワード
例外を呼び出し元に投げて処理を任せることができます。
IntegerクラスのparseInt()メソッドの定義の先頭部分である620行目以降をご覧ください。
Java17 標準APIpublic static int parseInt(String s, int radix) throws NumberFormatException { /* * WARNING: This method may be invoked early during VM initialization * before IntegerCache is initialized. Care must be taken to not use * the valueOf method. */ if (s == null) { throw new NumberFormatException("Cannot parse null string"); }
まず、メソッド名()の後ろの記述をご覧下さい。
throws NumberFormatException
このparseInt()メソッドはNumberFormatExceptionを投げる可能性のあることを示しています。そして、例えばnullを受け取ったらNumberFormatExceptionを投げるということを541行目で示しています。メソッドがスローする可能性がある例外を宣言することで、呼び出し元で例外をキャッチする必要があることを明示的に示し、プログラムの可読性と保守性を高めているのです。なお、複数の例外を投げる可能性がある場合は、カンマで区切って複数宣言することもできます。
throw new NumberFormatException("null");
throwとthrowsで“s”が付くか付かないかが分かりにいかも知れませんね。
やや強引ですが、
①実際に1つ1つの例外を投げる際には動詞のthrow
②このメソッドは(複数の)例外を投げる可能性のあるメソッドであるというときは名詞の複数形でthrows
と覚えていただくと比較的スムーズかと思います。
実験してみましょう。
package chap13;
public class Example09 {
public static void main(String[] args) {
System.out.println(Integer.parseInt(null));
}
}
<実行結果>
Exception in thread "main" java.lang.NumberFormatException: Cannot parse null string at java.base/java.lang.Integer.parseInt(Integer.java:630) at java.base/java.lang.Integer.parseInt(Integer.java:786) at chap13.Example09.main(Example09.java:6) |
確かにNumberFormatExceptionが発生しました。
この結果をスタックトレース情報というのだとこのテキストの1回目にお話しました。
1行目はどんな例外が発生したかというのを表し、2行目以降でその例外の発生源とその例外がどのように伝播していったかを示すのでした。その順番は、"スタック"トレースという名前のとおり、メソッドの呼び出し順を発生源から順に上に積んでいきます。
スタックトレース情報は上から下に見ていって、自作のクラスの自作のメソッドが怪しいのでそこを重点的に調べればOKです。
また、()の中の数字は例外発生の原因箇所の行番号です。クリックするとソースコードに飛べます。(ただし、かならずしもその行が例外発生の原因とは限りません。その周辺をチェックして下さい)
7.例外を呼び出し元に投げて処理を任せる
この仕組を使って自作のクラスでも例外処理を呼び出し元に投げる事ができます。
以下のExample10はサンプルプログラムは例外を呼び出し元に投げて処理を任せる例です。
コンストラクタで例外を発生させて呼び出し元のメインメソッドに投げています。受け取ったメインメソッドも例外を投げていますが、受取り手はJVMですので結局はスタックトレース情報を表示して終わっています。
package chap13;
public class Example10 {
public static void main(String[] args) throws Exception{
Sample s = new Sample();
}
}
package chap13;
public class Sample {
Sample() throws Exception {
throw new Exception();
}
}
<実行結果>
Exception in thread "main" java.lang.Exception at chap13.Sample.(Sample.java:6) at chap13.Example10.main(Example10.java:6) |
もちろんこのような無責任なプログラムは例示であって推奨しているわけではありません。誤解のないように。
なお、上記のプログラムでは例外の生成とthrowを1行で済ませていますのではじめは少し理解が困難かもしれません。あえて、2行で書くと以下のようになります。
Sample() throws Exception {
Exception e = new Exception();
throw e;
}
その他にオリジナルの例外クラスを作るというテーマでリンクに記事があります。余裕のある方は一読下さい。
まとめができたら、アウトプットとして演習問題にチャレンジしましょう。
以上、今回は「例外処理で想定外の事態に強いシステムにする」方法について見てきました。
例外処理を使うことでプログラムはトラブルの発生をユーザーに伝えることができ、途中終了を避けることができるのでした。また、継承の仕組みを上手く応用しているのも興味深いところでしたね。
実際にお客様の依頼で開発するアプリでは、詳細なエラー情報は画面に表示しないことが多いのです。特に深刻なエラーが起こった場合はなおさらです。
知識が不十分なお客様をいたずらに不安にさせることになりますし、下手をすれば責任問題に発展し、会社同士のもめ事にもなりかねないからです。
こういう場合はいつ、何が起こったのかを詳細にログに出力し、画面には、ただ「システム管理者に連絡して下さい」とだけ表示するようにします。
ですから、非チェック例外に対しても例外処理を何もしなくて良い、というわけでは決して無い、ということも、分かっておいてください。
次回は、「ArrayListでもっと配列を便利に使いこなす」です。
ArrayListを一言で言うと便利なメソッドを持った伸縮自在の配列です。現在では配列よりもよく使われています。
ということで、次回を楽しみに待っていてください。