新人エンジニア研修で知っておきたい例外の使い方
なぜ、例外の理解が重要なのか、その理由
この記事では、弊社の新人エンジニア研修の参考にJava8を解説します。
前回はインタフェースについて解説しました。
今回は例外処理について解説します。
例外処理も継承を巧みに使った仕組みの一例ですので復習にもなると思います。
この短い新人エンジニア研修の中でも例外がいくつか出てきました。
3.演算子のところで紹介したArithmeticException(標準API)
6.配列の作成と使用のところで紹介したArrayIndexOutOfBoundsException(標準API)
9.インスタンスの活用のところで紹介したNullPointerException(標準API)
10.継承(拡張)のところで紹介したClassCastException(標準API)
12.抽象クラスのところで紹介したNumberFormatException(標準API)
等がありましたね。
例外とは、人間に向けたプログラムからのトラブル報告書です。
このままでは処理を継続できないという報告です。
例えばあなたがお使いのゲームアプリが突然意味不明なメッセージを表示して落ちてしまったらどうでしょうか?
その上、何時間もかけて蓄積したセーブデータが残っていないとしたら。。。
エラーが発生しても続行するか、重要なデータを保存してから適切に終了するようにプログラミングしないといけません。
目次
1.try~catch文
例外処理の構文を以下に示します。
<構文>
try {
例外が投げられる可能性のある処理
}
catch(例外の型1 変数名1) {
例外の型1の例外が投げられたときの処理
}
catch(例外の型2 変数名2) {
例外の型2の例外が投げられたときの処理
}
finally {
最後に必ず行う処理
}
では、順番に解説していきます。
以下のサンプルプログラムを見てください。
<結果を表示する>
例外が発生しました
java.lang.ArithmeticException: / by zero
このプログラムを終了します
このように、例外が発生する恐れのある個所をtryブロックで囲みます。
catchの後ろの()の中には、あたかもメソッドの仮引数のように例外の型と変数を書きます。
変数名は何でも良いですが、errorの頭文字eやexceptionのexを使うのが慣例となっています。
catchブロックには例外が発生した時の処理を書きます。
なお、時々誤解する新人エンジニアがいますが、例外が発生した時の処理といっても何か有効なトラブル対応処理が書けるわけではありません。
最も多い処理は人間にトラブルの内容を知らせることです。
次にありえるのはデータベースやファイルをちゃんと閉じる処理です。
ここで変数eの中身を見ると例外の内容(原因)が分かります。
また、例外処理がなければここでプログラムが中断するところですが、今回は中断することなく最後まで終えることができています。
2.finally句の意義
finallyブロックにかかれた処理は例外発生の有無を問わず必ず実行されます。
ファイルのクローズやデータベースのクローズに使うというのが典型的な利用例です。
以下のサンプルプログラムを見てください。
<結果を表示する>
java.lang.NumberFormatException: For input string: "2byte"
このプログラムを終了します
例外が発生すると処理は一気にcatchブロックまで飛んでしまうため、8行目の処理が実行されません。
こんな時はどうしたらよいのでしょうか?
例えば以下のように同じ処理を2回記述するコードは冗長です。
そんな時は、finallyブロックを追加すれば繰り返しの記述を避けることができます。
<結果を表示する>
java.lang.ArithmeticException: / by zero
例外発生の有無にかかわらず実行したい処理です
このプログラムを終了します
finallyブロックにかかれた処理は例外発生の有無を問わず必ず実行されます。
当社の新人エンジニア研修の範囲でのfinallyの活用場面としては、データベース処理において例外が発生した場合に、データベースとの接続をクローズするという使い方があります。
この点は機会があればデータベースのところで紹介したいと思います。
3.複数例外のキャッチ
起こりうる複数の例外の場合に分けて、以下のように例外処理を書き分けることができます。
<結果を表示する>
java.lang.ArrayIndexOutOfBoundsException: 3
このプログラムを終了します
このようにtry~catch文は最初に発生した例外を捕捉します。
なお、Java 7からmulti-catch(マルチキャッチ)という構文が追加されています。
以下のような形になります。
4.例外クラスの体系
例によってIDEを使いArithmeticExceptionクラスから、スーパークラスをさかのぼって行きましょう。
まずはArithmeticExceptionクラスの41行目をみてください。
public class ArithmeticException extends RuntimeException {
となっていてRuntimeExceptionクラスのサブクラスであることが分かりました。
では次にRuntimeExceptionクラスの43行目を見てスーパークラスを探します。
public class RuntimeException extends Exception {
となっています。
このExceptionがすべての例外クラスの親玉(?)です。
さらにExceptionクラスの定義を45行目に見ると以下のようにThrowableクラスのサブクラスとして宣言されています。
public class Exception extends Throwable {
投げられる(Throwable)という意味のクラスですね。
このThrowableクラス(標準API)を調べると以下の記述があります。
Throwable
クラスは、Java言語のすべてのエラーと例外のスーパー・クラスです。
そして、Throwableのスーパークラスは例のObjectクラスです。
Throwableの直系の既知のサブクラスについて以下の記述があります。
クラスThrowable
- java.lang.Object
- java.lang.Throwable
- すべての実装されたインタフェース:
- Serializable
Exceptionクラスには兄弟分のErrorクラスがあるということですね。
つまり、ここまでを図示すると下図14.1のようになります。
この中でまず抑えるべきはその例外クラスが、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があります。
ただ、これはデータベースに関連した例外ですので、そこでお話ししたいと思います。

上記の例外クラスの体系を見て明らかなように以下のサンプルプログラムは意図した通りの処理を行います。
<結果を表示する>
java.lang.ArrayIndexOutOfBoundsException: 3
このプログラムを終了します
このように全ての例外クラスはそのスーパークラスであるExceptionクラス型で捕捉できるようになっています。
ただし、プログラマの意図が不明瞭になりがちですので手抜きで記述することは避けた方が良いでしょう。
※IDEからも複数のキャッチ句に置換しうることを提案されると思います。
具体的な例外を全てcatchして、それでも発生するかもしれない例外を補足する時に使います。
では、ここまでは例外をキャッチする側の処理を見てきましたので、逆に例外をスローする側の処理がどうなっているのかを探検してみましょう。
5.throwsキーワード
例外を呼び出し元に投げて処理を任せることができます。
IntegerクラスのparseIntメソッドの定義の先頭部分である532行目以降をご覧ください。
public 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("null"); }
まず、メソッド名()の後ろの記述をご覧下さい。
throws NumberFormatException
このparseIntメソッドはNumberFormatExceptionを投げる可能性のあることを示しています。
そして、例えばnullを受け取ったらNumberFormatExceptionを投げるということを541行目で示しています。
throw new NumberFormatException("null");
throwとthrowsで“s”が付くか付かないかが分かりにいかも知れませんね。
やや強引ですが、
①実際に1つ1つの例外を投げる際には動詞のthrow
②このメソッドは(複数の)例外を投げる可能性のあるメソッドであるというときは名詞の複数形でthrows
と覚えていただくとスムーズかと思います。
実験してみましょう。
<結果を表示する>
Exception in thread "main" java.lang.NumberFormatException: null
at java.lang.Integer.parseInt(Integer.java:542)
at java.lang.Integer.parseInt(Integer.java:615)
at chap14.Example08.main(Example08.java:6)
確かにNumberFormatExceptionが発生しました。
この結果をスタックトレース情報というのだとこのテキストの1回目にお話しました。
1行目はどんな例外が発生したかというのを表し、2行目以降でその例外の発生源とその例外がどのように伝播していったかを示すのでした。
スタックトレース情報は上から下に見ていって、自作のクラスの自作のメソッドが怪しいのでそこを重点的に調べればOKです。

6.例外を呼び出し元に投げて処理を任せる
この仕組を使って自分のクラスでも例外処理を呼び出し元に投げる事ができます。
以下のサンプルプログラムは、コンストラクタで例外を発生させて呼び出し元のメインメソッドに投げています。
受け取ったメインメソッドも例外を投げていますが、受取り手はJVMですので結局はスタックトレース情報を表示して終わっています。
<結果を表示する>
Exception in thread "main" java.lang.Exception
at chap14.Sample.<init>(Example09.java:13)
at chap14.Example09.main(Example09.java:6)
もちろんこのような無責任なプログラムは例示であって推奨しているわけではありません。
7.オリジナルの非検査例外を作成する
※この話題は研修時間の都合によりスキップすることがあります。
Exceptionクラスまたはそのサブクラスを継承したオリジナルの例外を作成することができます。
11.カプセル化のところで使ったカジノゲームの残高の例で説明をしてみます。
以下のサンプルプログラムを見てください。
OriginalExceptionという名前で非検査例外のオリジナル例外クラスを作成してみます。
RuntimeExceptionを継承すればいいですね。
記述は以下のようになります。
class OriginalException extends RuntimeException
RuntimeExceptionクラスのサブクラスでOriginalExceptionクラスを宣言しました。
コンストラクタはスーパークラスに丸投げしていますが、クラスの階層を上がっていってThrowableクラスまで行ってそこで詳細メッセージを初期化するようになっています。
次に、Player2クラスを作ります。
以前と同様にPlayer2は残高を1000ポイントを持っています。
そして残高を超えて引き出そうとすると例外を発生させ、「残高不足です」と表示させたいとします。
このとき全く新規にPlayer2を作ってしまうと、Playerクラスとの関係が不明確になります。
とはいえ以前のPlayerクラスを書き換えるというのは良くない選択肢です。
なぜなら、Playerクラスを使っているクラスが存在する(可能性がある)からです。
繰り返しになりますが、既に動いているクラスに手を加えることは極力避けましょう。
機能変更や機能追加をするときの武器として継承を使いましょう。
この考え方を「開放/閉鎖原則」といいます。(図14.3参照)
というわけでPlayer2クラスです。
注目していただきたいのは、OriginalExceptionのコンストラクタ呼び出しの実引数に文字列を渡しているところです。
トラブル報告書には、状況や原因が書かれていないといけません。
それをしているのがこのコンストラクタなのです。
この文字列がどのような実行結果として現れるかを後で確認してみて下さい。
また、
throw e;
という記述でOriginalExceptionのインスタンスを呼び出し元のメインメソッドに投げます。
最後は、メインメソッドの処理です。
最初に200ポイント引き出して、次に1000ポイントを引き出そうとします。
<結果を表示する>
現在の残高です:800
Exception in thread "main" chap14.OriginalException: 残高不足です
at chap14.Player2.withdraw(Player2.java:10)
at chap14.Example10.main(Example10.java:8)
コンストラクタ呼び出しの実引数に渡した文字列はここでメッセージとして使われます。
最後にメインメソッドで例外が発生し、キャッチされることなく処理を中断しています。

8.オリジナルの検査例外を作成する
※この話題は研修時間の都合によりスキップすることがあります。
OriginalExceptionクラスは非検査例外でした。
では、検査例外に変えてみると、どのようなことが起こるでしょうか?
といっても以下のようにExceptionクラス(RuntimeExceptionクラスではなく)を継承するだけであとは同じです。
class OriginalException2 extends Exception
以下のOriginalException2クラスを見てください。
そうするとPlayer3クラスはどのように変化するでしょうか?
以下のようにtry catchを付ける必要が出てきます。
あるいは、try catchを付けずにメソッドの宣言を以下のように変えても良いです。
public void withdraw(int amount) throws OriginalException2
このメソッド宣言の意味は、例外処理を呼び出し元に投げるということでした。
ただし、 throws以降も含めてメソッドのシグネチャになるため、この場合はwithdrawのオーバーライドにはなりません。
この記述はOriginalException2という例外を投げる可能性のあるメソッドだということを示しているのでした。
もしも、これを書かないと例外の発生源であるこのwithdrawメソッド内でtry-catchしなけばならないというのは前述のとおりです。
<結果を表示する>
現在の残高です:800
chap14.OriginalException2: 残高不足です
例外処理はトラブルが起こったことの報告でした。
会社のトラブルの報告でも、直接対応した社員から上司に報告を上げることがありますね。
同じように、Javaでは例外の報告を使用者に報告するのが基本原則です。
例外を発生源で握りつぶすのではなく。
つまり、ここまでをまとめると、
例外のインスタンスをthrowした後の処理は2通りあります。
- try~catch文で囲んでその場で処理する
- メソッドの呼び出し側に投げて処理を任せる
そして、②の場合はメソッドの記述が以下のように変化しました。
<構文>
戻り値 メソッド名(引数) throws 例外の型 {
例外を投げる可能性のあるメソッドの内容
}
9.極力標準APIに備わっている例外クラスを使う
なお、今回は説明のため、あえて自分でオリジナルな例外クラスを作成しましたが、本当は極力標準APIに備わっている例外クラスを使うべきです。
なぜなら、標準APIの例外クラスならば、Javaプログラマーの間でどういう例外なのかの共通理解が得られているからです。
例えば引数が不適切な場合のIllegalArgumentException(標準API)やフィールドの状況等が不適切な場合のIllegalStateException(標準API)が代表例です。
それぞれ、Illegal「不正な」Argument「引数」ExceptionとIllegal「不正な」State「状態」Exceptionという意味です。
これら2つの例外クラスを使ってPlayer4クラスを作成してみました。
マイナスの金額を引き出すテストをしてみます。
<結果を表示する>
出金額は正の整数のみです
残高(1000)を超えて引き出すテストをしてみます。
<結果を表示する>
残高不足です
このように意図した通りになりました。
今回は例外処理について見てきました。
例外処理を使うことでプログラムはトラブルの発生をユーザーに伝えることができ、途中終了を避けることができるのでした。
また、継承の仕組みを上手く応用しているのも興味深いところでしたね。
次回のテーマはArrayListとジェネリクスです。
ArrayListを一言で言うと便利なメソッドを持った伸縮自在の配列です。
ということで、次回を楽しみに待っていてください。
<まとめ:隣の人に正しく説明できたらチェックを付けましょう>
□try~catch文の記述が任意である例外を非検査例外という
□検査例外としては、例えばSQLExceptionがある
□例外を呼び出し元に投げて処理を任せることができる
□スタックトレース情報は上から下に見ていって、自作のクラスの自作のメソッドが怪しいのでそこを重点的に調べる
□throwされた例外のインスタンスの処理方法は、①try~catch文で囲んでその場で処理する、②メソッドの呼び出し側に投げる、の2つがある
まとめができたら、アウトプットとして演習問題にチャレンジしましょう。
【今回の復習Youtube】
JavaSE8の解説に戻る
最後までお読みいただきありがとうございました。