プログラムにおいて処理を続行することが不可能なエラーが発生した場合には、例外処理を行います。
例外処理は、本来実行したい処理とは異なる部分ですが、全く記述しなかったり、不適切に記述すると、エラー発生時にシステムが停止してしまったり、必要な情報を残すことができなかったりするなどの重大な事態を招いてしまう恐れがあります。
ですから、実際の開発現場でプログラミングをするには、必須の処理となります。
本記事では、Javaにおける例外処理についてまとめます。
プログラミング初心者の方の学習や、忘れてしまった方の復習として、参考にしていただければ幸いです。
記載しているプログラムは、Java11を使って動作確認をしています。
Javaの例外処理
Javaの例外処理に関連する記述は以下のとおりです。
- try…catch文:tryブロック内で発生した例外をcatchで補足する。
- throw文:プログラマーが意図的に例外処理を発生させる。
- finally文:例外が発生してもしなくても際にも必ず実行される処理。
try…catch文
例外が発生する可能性がある文をtryブロックに記述します。
例外が発生すると、tryブロックの例外発生以降の処理はスキップされ、catchブロックの中が実行されます。
public static void main(String[] args) { try { System.out.println("before exception."); throwTestException(); System.out.println("after exception."); } catch (Exception e) { System.out.println(e.getMessage()); } }
上記のmainメソッドでは、throwTestExceptionというメソッドを呼び出し、そこでは必ず例外が発生するとします。
例外が発生すると、それ以降の処理はスキップして、catchブロックの処理が実行されます。
つまり、上記のプログラムでは、”before exception.”が表示された後に、”after exception.”は表示されず、catchブロックのe.getMessage()の内容が表示されます。
catch文には捕捉する例外クラスを指定します。複数種類の例外が発生する可能性がある場合には、いくつも指定することができます。
catchブロックの処理が異なる場合には、catch文を複数記述します。
public static void main(String[] args) { try { System.out.println("before exception."); throwTestException(); System.out.println("after exception."); } catch (IOException e) { System.out.println("IO error."); } catch (Exception e) { System.out.println(e.getMessage()); } }
catchブロックの処理が同じである場合には、1つのcatch文に複数の捕捉対象のクラスを指定します。
public static void main(String[] args) { try { System.out.println("before exception."); throwTestException(); System.out.println("after exception."); } catch (SQLException | IOException e) { System.out.println(e.getMessage()); } }
Javaはコンパイルが必要な言語ですので、発生しない例外クラスをcatch文に指定しても、コンパイルエラーとなります。
実行前に間違いに気づける、不要なコードを気づけるというのは、Javaの良いところであると思います。
throw文
throw文を使うことで、プログラマーは任意の場所で例外を発生させることができます。
発生させる例外の種類が後述するチェック例外である場合には、メソッド定義の最後にthrowsを付け、発生させる例外クラスを指定する必要があります。
複数種類の例外が発生する場合には、カンマで区切って複数を指定します。
public static void throwTestException() throws Exception { System.out.println("before throw."); throw new Exception("Test Exception."); // System.out.println("after throw."); // -> 到達不能コードとしてコンパイルエラーとなる }
上記のthrowTestExceptionメソッドでは、このメソッドが呼び出されると必ず例外が発生します。
例外が発生すると、それ以降の処理はスキップして、このメソッドを抜けて呼び出し元の処理に戻ります。
つまり、上記のプログラムでは、”before throw.”が表示された後に、必ず例外が発生し、このメソッドを抜けます。
例外発生後の処理として、System.out.println文を記載していますが、必ず例外が発生するような文の後に処理を記述しても、到達しない処理としてコンパイルエラーとなります。
finally文
finally文を使うと、例外が発生してもしなくても、どちらの場合でも必ず処理を実行させることができます。
public static void main(String[] args) { try { System.out.println("before exception."); throwTestException(); System.out.println("after exception."); } catch (Exception e) { System.out.println("catch exception."); } finally { System.out.println("finally."); } }
上記の例では、例外が発生しない場合は、”before exception.” → “after exception.” → “finally.”の順に表示され、tryブロックの後に、finallyブロックが処理されます。
例外が発生した場合は、”before exception.” → “catch ecxeption.” → “finally.”の順に表示され、tryブロック(例外発生地点まで)、catchブロックの後に、finallyブロックが処理されます。
finallyでは、通常例外が発生しても、しなくても必ず必要となる、リソース開放などの事後処理を記述することが多いです。
プログラムの構成によっては、catchブロックは記述せずに、try…finally文だけ記述する場合もあります。
例外オブジェクト
catch文では、例外情報として例外オブジェクトを補足することができます。
public static void main(String[] args) { try { System.out.println("before exception."); throwTestException(); System.out.println("after exception."); } catch (Exception e) { System.out.println(e.getMessage()); } }
上記の例では、eという変数に補足したオブジェクトを格納し、標準出力にeが持つメッセージを表示しています。
catcht文で指定したクラスの例外を捕捉しますが、いずれにも該当しない例外クラスの場合は、さらに呼び出し元にthrowされます。
逆に、自分でthrowする場合には、任意の型のオブジェクトを例外情報として指定することができます。
public static void throwTestException() throws Exception { throw new Exception("Test Exception."); }
上記の例では、Javaが標準で提供している、Exceptionクラスを使用しています。
エラーの種類を区別するために、Javaでは標準でいくつかの例外オブジェクトを提供していますが、大きくチェック例外と非チェック例外に分類されます。
例外の種類 | チェック例外 | 非チェック例外 | |
---|---|---|---|
実行時例外 | 重大なエラー | ||
説明
|
主にプログラムの外で発生し、プログラムでは回避できない例外。
|
主にプログラムの不具合等により、呼出し側に原因がある場合に発生する例外。
|
アプリケーションでcatchすべきでない重大なエラー。
|
対象例外クラス
|
RuntimeExceptionを除く、Exceptionクラス配下のクラス
|
RuntimeExceptionクラス配下のクラス
|
Errorクラス配下のクラス
|
記述
|
catch文に指定する必要がある。
上位にthrowする場合は、throwsに指定する必要がある。
|
catch文に指定して捕捉することも出来るが、事前にチェックする等により回避可能であるため、基本的には記述しない。
throws文にも指定しなくて良い。
|
基本的に記述しない。(捕捉してもプログラムではどうしようもない)
|
発生する場面
|
ファイル操作の失敗や外部リソース(DB等)へのアクセスに失敗した場合など
|
nullの参照や配列のインデックスを超える領域に対するアクセスした場合など
|
Java仮想マシンが壊れた場合や、動作を継続するのに必要なリソースが足りないなど
|
主な例外クラス
|
・IOException
・SQLException
|
・NullPointerException
・IndexOutOfBoundsException
|
・OutOfMemoryError
・StackOverflowError
|
Javaの標準で提供されている例外クラスを自分でthrowする時に、指定することもできます。
適切なエラーの種類がない場合は、独自の例外オブジェクトを作成することも可能です。
開発現場によっては、独自の例外オブジェクトを用意していて、発生したケースに応じて使い分けたりします。
独自の例外オブジェクトを作成する際には、基本的にExceptionクラスを継承して作成することが多いです。
ログ出力
例外が発生した際に、後からなぜ発生したのかを調査する際に重要となるのがログです。
開発中はデバッグなどにより原因を特定することができますが、本番運用が始まるとデバッグなどはできないので、ログだけが頼りになります。
原因を特定するためには、原因が分かるようなログを出力する必要があります。
その1つがスタックトレースです。
スタックトレースは、プログラムを処理してきた順序(メソッドの呼び出し階層)が記録されていて、どこで例外が発生したのかを特定するために必要な情報となります。
Javaでスタックトレースを取得するには、例外オブジェクトのgetStackTrace()を呼び出します。直接標準エラー出力に出力するには、printStackTrace()を呼び出します。
public static void main(String[] args) { try { System.out.println("before exception."); throwTestException(); System.out.println("after exception."); } catch (Exception e) { e.printStackTrace(); } }
上記のプログラムでは、catchブロックで標準エラー出力にスタックトレースを出力しています。
スタックトレースには以下のような情報が表示され、どのプログラムの何行目で例外が発生したのかを特定することができます。
java.lang.Exception: Test Exception. at ExceptionJava.throwTestException(ExceptionJava.java:17) at ExceptionJava.main(ExceptionJava.java:6)
例外発生時のログ出力には、基本的にこのスタックトレースとともに、他にデータを特定できる情報(レコードのIDなど)や引数の情報(不正なデータが渡されていないか)などを出力することが多いかと思います。
どんな情報をログ出力すべきかは、経験によるところもあるのですが、自分が調査する立場ならどんな情報が必要となりそうかを考えてみると良いと思います。
例外処理は障害発生の最後の砦
プログラムは必ずと言ってよいほどエラーが発生します。
プログラム自体が完璧であったとしても、連携しているデータベースやネットワーク、OSなどあらゆるところで想定しないエラーが発生します。
プログラマーとしては、あらゆる事態を想定し、エラーが発生してもなるべく影響を少なくし、原因調査のために必要な情報を残すという点を心がける必要があります。
僕個人としては、正常系のプログラムであれば誰でも書けるが、この例外処理を無駄なく適切に書けるプログラマーが良いプログラマーであると思っています。
今回は、Javaの例外処理についてまとめました。
参考になれば幸いです。
コメント