Edited at

Javaの例外処理について

More than 1 year has passed since last update.


Javaの例外処理について

記述したプログラムに期待していない動作が起きたことを例外と呼びます。

ほとんどのプログラミング言語には、例外を検知して対応するための仕組みが用意されています。

Javaにおける例外処理について、個人的に曖昧なままの部分があったため、その不明点を解消するためにまとめました。


Javaの例外の種類

Javaにおける例外には以下の3種類があります。


  • 検査例外(Exception)

  • 実行時例外(RuntimeException)

  • エラー(Error)


検査例外(Exception)

検査例外は、プログラム中で捕捉しなければならない例外です。

これは、プログラムの作成時に想定できる異常を通知するために使用します。

検査例外を用いると、コンパイル時に想定される異常に対応する処理が存在するかをチェックすることができるため、堅牢なアプリケーションを作成することができます。

検査例外は、プログラムで捕捉(catch)して処理するか、上位の呼び出し元に対して例外を発生させる。(throwする)ことが必須となります。

上記にいずれも行わない場合には、コンパイルエラーが発生します。


実行時例外(RuntimeException)

実行時例外は、プログラム中で捕捉する必要のない例外です。

主にプログラム作成時には想定されていないエラーを通知するために使用します。

その原因は、往々にしてバグや設定ミスなどです。

検査例外とは異なり、プログラムで捕捉しない場合にもコンパイルエラーは発生しません。

捕捉しない場合には、無条件で呼び出し元で発生します。

検査時例外とは異な理、実行事例外を発生させるメソッドを利用する場合は、呼び出し元でその例外を捕捉する必要はありません。

実行時例外を利用することにより、想定外の動作により異常が発生する場合でも、不必要に呼び出し元にて例外を捕捉させる必要がなくなります。


エラー(Error)

エラーは、プログラムで捕捉すべきではないものです。

例外とは異なり、システムの動作を継続できない致命的なエラーを示します。

エラーが起こった際には、Javaが速やかにプログラムを終了させます。


Javaの例外を表す3つのクラス

Javaにおける3種類の例外はそれぞれ対応するクラスが存在します。

それらは、以下のような階層構造を持っています。

例外クラスの階層構造


java.lang.Exceptionクラス

検査例外を表すクラスです。

このクラスを継承した例外には例えば次のようなものがあります。


  • java.io.IOExceptionクラス:ファイルやネットワーク等の入出力中のエラーを表す

  • java.sql.SQLExceptionクラス: データベースアクセス中に発生したエラーを表す

このクラスを継承した例外は、プログラム中で捕捉するか、発生するメソッドのシグネチャでthrows節を記述する必要があります。

但し、後述のRuntimeExceptionクラス及びその継承クラスは除きます。

以下のコードは、ファイルの読み込み処理を行うメソッドのシグネチャです。


readFile.java


public List<String> readFile() throws IOException {
// ... ファイル読み込み処理
}

上記のようにシグネチャで例外が記述されているメソッドは、呼び出す側でその例外を処理するコードを記述していなければ、コンパイルエラーがとなります。

このようにthrows節をメソッドのシグネチャに記述することで、例外が起こることを想定していることがわかります。

そうやって処理しなければならない例外を言語仕様としてチェックすることで、ライブラリの使用時や、大規模開発などでもミスの起こりにくいプログラムを作りやすくなります。


java.lang.RuntimeExceptionクラス

実行時例外を表すクラスです。

このクラスを継承した例外には、例えば以下のようなものがあります。


  • java.lang.NumberFormatException: Integer#parseIntに整数に変換できない文字列を渡した場合になどに発生する

  • java.lang.ArithmeticException: 0除算をした場合に発生する

このクラスを継承した例外は、プログラム中で必ずしも捕捉する必要はなく、したがってメソッドのシグネチャにthrows節を記述する必要もありません。

catchブロックもthrows節も記述しなかった場合は、発生した実行時例外は自動的に呼び出し元に伝搬していきます。

ただし、スレッドを開始させた処理自体には、例外は伝搬しません。

実行時例外をどこでも捕捉しなければ、最終的に例外が発生したスレッドが終了します。


Java.lang.Errorクラス

通常のアプリケーションでは捕捉すべきでない重大な問題を示すクラスです。

Javaの例外機構の観点からすると、ErrorはRuntimeExceptionと似ていて、catchブロックもthrows節も記述する必要がありません。

しかしRuntimeExceptionとは異なり、Errorは捕捉すべきではないものです。

Errorが発生する状況は、アプリケーションが異常自体に陥っており、速やかにプログラムを終了すべきだからです。

有名なErrorの1つに、java.lang.OutOfMemoryErrorクラスがあります。

これはJavaで使うメモリが足りないなどの場合に発生します。このエラーが発生した場合には、ログ出力さえできない状態になっていることが考えられます。

そう行った状態でアプリケーションが沈黙するのは最悪の事態といえるので、速やかに終了すべきなのです。


例外処理の構文

例外処理を捕捉し、処理を行う構文には3つの種類があります。


  • try...catch...finally

  • try-with-resources

  • multi catch


try...catch...finally

もっとも基本的な例外処理の構文は、try...catch...finally構文です。


tryCatchFinally.java

try {

// SomeException例外が発生するコードを含む処理
} catch (SomeException e) {
// SomeException例外を捕捉した場合の処理
} finally {
// try...catchブロックを終了する際に必ず実行すべき処理
}

tryブロック内に記述する処理は必要最低限のものにします。

あまり長大な処理をtryブロックの中に入れてしまうと、catchブロックで捕捉した例外がどこで発生したものなのかがコードから読み取れなくなってしまうためです。

try...catch...finallyの各ブロックをまたいで変数を参照する必要がある場合は、tryブロックの直前に変数を宣言しておきます。


declearVariableBeforeTry.java

InputStream is = null;

try {
Path path = Paths.get("/your/file/path.txt")
is = Files.newInputStream(path);
is.read();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (is != null ) {
is.close();
}
}

finallyブロックは、ストリームやデータベース接続のように、使用後に必ず解放をしなければならないリソース(OSで確保される資源)のオブジェクトを用いる場合によく使われます。

finallyブロックに記述された処理は、try...catchブロックの終了時に必ず一度実行されるためです。


try-with-resource

try...catch...resourceの構文は、Java6までの定石でしたが、finallyブロックの記述が冗長になると言う欠点がありました。

特に、リソースを複数利用する場合などはそれが顕著に現れます。

そこで、Java7からは同等の処理を行う新たな構文が導入されました。


tryWithResouce.java

Path path = Paths.get("/your/file/path.txt");

try (InputStream is = Files.newInputStream(path)) {
is.read();
} catch (IOException e) {
e.printStackTrace();
}

finallyブロック内でリソースを解放する処理の記述がないことがわかります。

Java7からは、InputStreamなどのリソースを扱うクラスは、java.lang.AutoClosableインターフェースまたはjava.io.Closableインターフェースを実装するようになりました。

そしてtryブロックの開始時にAutoClosableインターフェースの実装クラスを宣言しておくと、そのtry...catchブロックの終了時の処理を行うcloseメソッドを自動的に呼び出してくれるのです。

なお、tryブロックの開始時に記述する宣言は、複数の文を記述できます。


multiTryWithResource.java

try (InputStream is = new FileInputStream(fromFile);

OutPutStream os = new FileOutputStream(toFile)) {
is.read();
os.write();
} catch (IOException e) {
e.printStackTrace();
}

ただし、tryブロック開始時の宣言部にリソースの確保/解放に関係のない処理を書くことは、プログラムの可読性を損なうためにするべきではありません。


multi catch

tryブロックの中では複数の種類の例外が発生し得ます。

そのため、それぞれの例外に応じた処理を行いたい時のためのmulti catch構文が存在します。


multiCatch.java

try {

Class<?> clazz = class.forName(className);
SomeClass someInstance = clazz.newInstance();
} catch (ClassNotFoundException |
InstantiationException |
IllegalAccessException e) {
e.printStackTrace();
}

このように書くと、ClassNotFoundException, InstantiationException, IllegalAccessExceptionのどれが発生した場合にも、同じcatchブロックでエラー処理が行われるようになります。

catchブロックを複数書くこともできるため、例外処理を分ける必要がある場合は、それぞれ別々のcatchブロックを書くようにします。


multiCatchBlock.java

try {

Class<?> clazz = class.forName(className);
SomeClass someInstance = clazz.newInstance();
} catch (ClassNotFoundException) {
// ClassNotFoundExceptionに対する例外処理
} catch (InstantiationException |
IllegalAccessException e) {
// InstantiationExceptionとIllegalAcessExceptionに対する例外処理
}



例外処理を行う際のポイント


エラーコードをreturnしない

メソッドに処理を依頼した結果を、エラーコードの戻り値として受け取るパターンは、アンチパターンです。

それは以下のような理由からです。


  • 値をどこで定義するか不定となる

  • 値が追加された場合のメンテナンスコストが大きい

Javaでは例外機構が言語仕様として提供されているため、エラーが発生した場合には例外を発生させるべきです。

正常に処理が終了したのなら、そのオブジェクトを戻り値で返すことで、呼び出し元から見た際には、戻り値が得られたら、処理自体は成功したと考えられるためです。


例外をもみ消さない

例外をcatchブロックで捕捉したのちの処理は、プログラマが自由に扱うことができます。

そのため、例外が捕捉した際の処理の指針を持っておくことは重要です。

基本的には、以下の観点を守ると良いとされます。


  • ログ出力を忘れない

  • 処理を継続するか判断する


ログ出力を忘れない

例外をログ出力する際には、できる限り例外のスタックトレースをログに出力させるようにします。


logStackTrace.java

// Logger log = ...

String strValue = "abc";
try {
int intValue = Integer.valueOf(strValue);
System.out.println("intValue is " + intValue);
} catch (NumberFormatException e) {
log.warn("数値ではありません", e);
}


こうすることで、例外発生時の値、例外発生までのメソッドの呼び出し階層などの情報が得られます。


処理を継続するか判断する

例外処理では、例外発生後の処理を継続するかを明確にすることも重要です。

ほとんどの場合、例外が発生したら後続処理を中止して、直ちに復旧するか上位メソッドに例外をthrowすることになります。

それでも処理を継続する場合は、デフォルト値を与えるなどしてそれ以降の処理を継続しても大丈夫なようにします。


continueProcess.java

// Logger log = ...

// Property props = ...

String strValue = null;
try {
strValue = props.get("key");
} catch (IOException e) {
log.warn("プロパティの読み込みに失敗しました", e);
strValue = "default";
}

if (strValue.length() < 5) {
log.error("5文字異常が必要");
return;
}



throws Exception感染を避ける

複数の例外が発生するが、例外の宣言や捕捉が面倒だと言う理由で、メソッドのシグネチャにthrows Exceptionと記述することは、後に面倒なことになるためにやめましょう。

具体的には、以下のような弊害があります。


  • 呼び出し元でExceptionを捕捉しなければならなくなる

  • 途中で具体的な例外が発生するとしても、Exceptionに巻き込まれる

  • 途中でRuntimeExceptionが発生するとしても、Exceptionに巻き込まれる


どの階層で例外を捕捉して処理すべきか

基本的な考え方としては、次の2種類に分けて考えると良いでしょう。


  • 例外が発生する可能性がある箇所

  • 処理の流れを判断する箇所

前者の例外が発生する箇所で、個別に処理の中止や回復の判断を行うと全体的な流れが見えなくなってしまいます。

また、個別に判断しようとすると大変な数の例外を相手にしなければならなくなります。

そのため、末端の処理では例外を発生させるだけに留めると良いとされます。

後者の処理の流れを判断する箇所は、末端の処理から発生した例外を捕捉するべきところであり、処理を継続するか、中止するかはこの部分で判断すべきです。

大きなプログラムの場合には、処理の塊としての階層が出来上がると思います。

その場合には、できるだけ上位の呼び出しもと階層で例外を捕捉するようにした方が扱いやすい場合が多いでしょう。

いずれにしても、リソースの後始末はtry...catch...finallyブロックで忘れずに行いましょう


独自例外を作成する

通常、特に理由がなければJavaの標準APIにある例外クラスから適切なクラスを選択して使用するのが良いでしょう。

しかし、例外を個別に処理するのではなく、統一的に処理したい場合があります。

その時に利用するのが独自例外です。

よく作成する独自例外は以下の2つになります。


  • アプリケーション例外

  • システム例外


アプリケーション例外

アプリケーションのビジネスロジックでエラーが発生したことを示す例外として作成します。

Webアプリケーションの場合、該当の処理をやり直すことができるエラーがこれに当たります。

例えば、画面から入力したデータがシステムの期待するフォーマットにあっていない場合などに、ApplicationIllegalFormatExceptioなどのように具象クラスを作成したりします。


システム例外

データベースが停止している、ネットワークが繋がらないなど、システムとして正常動作を継続できない場合などに、システム全体に影響がある障害が発生したことを示す例外として作成します。

以下のような場合には、独自例外を作るべきと言えます。


  • 業務に特化した処理である

  • フレームワークやシステムで共通的な例外処理をする

上記の条件を満たす場合に独自例外を導入すると次のメリットがあると考えられます。

- Javaの標準APIにある例外クラスと区別することで、例外を捕捉する側が多くの例外を意識しなくて済むようになる

- 業務ロジックとして共通処理を作る際に、影響範囲を局所化することができる

反対にいえば、例外処理を行うロジックが業務に依存しない場合には、標準APIにある例外を使うべきです。

独自例外は、それぞれ以下のようにして作成します。


  • 検査例外: java.lang.Exceptionクラスを継承

  • 実行時例外: java.lang.RuntimExceptionクラスを継承

簡単な独自クラスは以下のようになります。

java.lang.Exceptionクラスには、引数なしのコンストラクタなどがありますが、独自クラスでは、できる限り引数を強制する形にするべきです。

なぜならば、例外はデバッグに必要な情報になるため、開発者にメッセージや発生の原因となった例外を指定してもらうようにするべきだからです。


extendedException.java

public class ApplicationException extends Exception {

public ApplicationException(String message) {
super(message);
}

public ApplicationException(String message, Throwable cause) {
super(message, cause);
}

public ApplicationException(Throwable cause) {
super(cause);
}
}


メッセージの代わりにエラ−IDとパラメータを指定することで、実際のメッセージ等をエラーIDに対応する情報としてプログラム外に追い出すようにする設計もできます。

これは、Java本格入門にておすすめされていました。


extendedExceptionWithIdAndParams.java

public class ApplicationException extends Exception {

private String id;
private Object[] params;

public ApplicationException(String id, Object... params) {
super();
this.id = id;
this.params = Arrays.copyOf(params, params.length);
}

public ApplicationException(String id, Throwable cause, Object... params) {
super(cause);
this.id = id;
this.params = Arrays.copyOf(params, params.length);
}

public String getId() {
return id;
}

public Object[] getParams() {
return Arrays.copyOf(params, params.length);
}
}



Javaの標準APIの代表的な例外の一覧


java.lang.ArrayIndexOutOfBoundsException

配列のindex指定に問題があります。


java.lang.IndexOutOfBoundsException

コレクションのindex指定に問題があります


java.lang.ClassCastException

キャストできない型にキャストしています。


java.lang.IllegalArgumentException

引数に問題があります


java.lang.NullPointerException

nullを保持している変数のメンバを呼び出しました。


java.lang.NumberFormatException

数値変換できません。


参考文献