Javaにおける例外処理
以下は日本語で自然な表現でのパラフレーズです(一つのオプションのみ):
イントロダクション
例外は、プログラムの実行中に発生し、通常のフローを妨げるエラーイベントです。Javaでは、Java例外処理として知られる例外シナリオを堅牢でオブジェクト指向の方法で処理する手段が提供されています。
Javaにおいて、例外は様々な状況から発生する可能性があります。例えば、ユーザーが間違ったデータを入力した場合、ハードウェアの故障、ネットワーク接続の失敗、またはダウンしているデータベースサーバーなどです。特定の例外のシナリオに対して何をすべきかを指定するコードは例外処理と呼ばれます。
例外のスローとキャッチ
Javaは、ステートメントの実行中にエラーが発生した場合には例外オブジェクトを作成します。この例外オブジェクトには、メソッドの階層構造や例外が発生した行番号、例外の種類など、多くのデバッグ情報が含まれます。
メソッド内で例外が発生すると、例外オブジェクトを作成してランタイム環境に渡すプロセスを「例外を投げる」と呼びます。プログラムの通常のフローが停止し、Javaランタイム環境(JRE)が例外のハンドラーを探そうとします。例外ハンドラーは、例外オブジェクトを処理するためのコードのブロックです。
- The logic to find the exception handler begins with searching in the method where the error occurred.
- If no appropriate handler is found, then it will move to the caller method.
- And so on.
もしもメソッドの呼び出しスタックがA->B->Cであり、メソッドCで例外が発生した場合、適切なハンドラの検索はC->B->Aへと移動します。
適切な例外ハンドラが見つかった場合、例外オブジェクトはハンドラに渡され、処理されます。ハンドラは「例外を捕捉する」と言われます。適切な例外ハンドラが見つからない場合、プログラムは終了し、例外に関する情報をコンソールに出力します。
Javaの例外処理フレームワークは、ランタイムエラーのみを処理するために使用されます。コンパイル時のエラーは、コードを書く開発者によって修正されなければならず、それをしないとプログラムは実行されません。
Javaの例外処理キーワード
Javaでは、例外処理を目的とした特定のキーワードが提供されています。
-
- throw – エラーが発生した場合、例外オブジェクトが作成され、Javaランタイムはそれらを処理するために処理を開始します。コード内で明示的に例外を生成したい場合があります。たとえば、ユーザー認証プログラムでは、パスワードがnullの場合にクライアントに例外を投げる必要があります。throwキーワードは、例外をランタイムに投げて処理させるために使用されます。
throws – メソッド内で例外を投げて処理しない場合、メソッドシグネチャにthrowsキーワードを使用して、呼び出し側のプログラムにメソッドで発生する可能性のある例外を知らせる必要があります。呼び出し側のメソッドは、これらの例外を処理するか、throwsキーワードを使用して呼び出し側のメソッドに伝えることができます。throws節には複数の例外を指定でき、main()メソッドとも使用できます。
try-catch – コードで例外処理にはtry-catchブロックを使用します。tryはブロックの開始であり、catchはtryブロックの最後にあり、例外を処理します。tryブロックには複数のcatchブロックを持つことができます。try-catchブロックはネストすることもできます。catchブロックには、例外型であるパラメータが必要です。
finally – finallyブロックは省略可能で、try-catchブロックとのみ使用できます。例外は実行プロセスを停止するため、クローズされないままのリソースがある場合にfinallyブロックを使用できます。finallyブロックは常に実行されます。例外が発生したかどうかに関係なく。
例外処理の例
package com.scdev.exceptions;
import java.io.FileNotFoundException;
import java.io.IOException;
public class ExceptionHandling {
public static void main(String[] args) throws FileNotFoundException, IOException {
try {
testException(-5);
testException(-10);
} catch(FileNotFoundException e) {
e.printStackTrace();
} catch(IOException e) {
e.printStackTrace();
} finally {
System.out.println("Releasing resources");
}
testException(15);
}
public static void testException(int i) throws FileNotFoundException, IOException {
if (i < 0) {
FileNotFoundException myException = new FileNotFoundException("Negative Integer " + i);
throw myException;
} else if (i > 10) {
throw new IOException("Only supported for index 0 to 10");
}
}
}
- The testException() method is throwing exceptions using the throw keyword. The method signature uses the throws keyword to let the caller know the type of exceptions it might throw.
- In the main() method, I am handling exceptions using the try-catch block in the main() method. When I am not handling it, I am propagating it to runtime with the throws clause in the main() method.
- The testException(-10) never gets executed because of the exception and then the finally block is executed.
printStackTrace()はデバッグ目的でExceptionクラスの中で使われる便利なメソッドの一つです。
このコードは次のように出力されます。
java.io.FileNotFoundException: Negative Integer -5 at com.scdev.exceptions.ExceptionHandling.testException(ExceptionHandling.java:24) at com.scdev.exceptions.ExceptionHandling.main(ExceptionHandling.java:10) Releasing resources Exception in thread “main” java.io.IOException: Only supported for index 0 to 10 at com.scdev.exceptions.ExceptionHandling.testException(ExceptionHandling.java:27) at com.scdev.exceptions.ExceptionHandling.main(ExceptionHandling.java:19)
いくつかの重要なポイントに注意する必要があります。
- We can’t have catch or finally clause without a try statement.
- A try statement should have either catch block or finally block, it can have both blocks.
- We can’t write any code between try-catch-finally blocks.
- We can have multiple catch blocks with a single try statement.
- try-catch blocks can be nested similar to if-else statements.
- We can have only one finally block with a try-catch statement.
Javaの例外の階層構造
前述の通り、例外が発生すると例外オブジェクトが作成されます。Javaの例外は階層的であり、継承を利用してさまざまな種類の例外を分類しています。ThrowableはJavaの例外階層の親クラスであり、その下にはErrorとExceptionという2つの子オブジェクトがあります。Exceptionはさらに、チェックされる例外と実行時例外に分けられます。
-
- エラー:エラーは、アプリケーションの範囲外で予測や回復が不可能な例外的なシナリオです。たとえば、ハードウェアの故障、Java仮想マシン(JVM)のクラッシュ、またはメモリ不足エラーなどが挙げられます。そのため、エラーには別個の階層があり、これらの状況を処理しようとしてはいけません。一般的なエラーの例として、OutOfMemoryErrorやStackOverflowErrorがあります。
チェック例外:チェック例外は、プログラムで予測し、回復を試みることができる例外的なシナリオです。たとえば、FileNotFoundExceptionなどです。この例外をキャッチして、ユーザーに有用なメッセージを表示し、デバッグ目的で適切にログを取る必要があります。Exceptionは、すべてのチェック例外の親クラスです。もしもチェック例外をスローする場合は、同じメソッド内でそれをキャッチするか、throwsキーワードを使用して呼び出し元に伝える必要があります。
実行時例外:実行時例外は、プログラミングのミスによって引き起こされます。たとえば、配列から要素を取得しようとすることです。要素を取得する前に配列の長さをチェックする必要があります。そうしなければ、ランタイム時にArrayIndexOutOfBoundExceptionがスローされる可能性があります。RuntimeExceptionは、すべての実行時例外の親クラスです。メソッドで実行時例外をスローする場合、メソッドのシグネチャのthrows節にそれを明示する必要はありません。実行時例外は、より良いプログラミングによって回避することができます。
例外クラスの便利なメソッドのいくつか
JavaのExceptionとそのすべてのサブクラスは、特定のメソッドを提供せず、すべてのメソッドは基本クラスであるThrowableに定義されています。例外クラスは、さまざまな種類の例外シナリオを指定するために作成されており、ルート原因を簡単に特定し、タイプに応じて例外を処理することができます。Throwableクラスは、相互運用性のためにSerializableインターフェースを実装しています。
Throwableクラスの便利なメソッドの一部は次の通りです:
-
- public String getMessage() – このメソッドは、ThrowableのメッセージStringを返し、例外が作成される際にメッセージを提供することができます。
-
- public String getLocalizedMessage() – このメソッドは、サブクラスがオーバーライドして呼び出しプログラムにローカライズされたメッセージを提供するために提供されています。Throwableクラスの実装では、このメソッドはgetMessage()メソッドを使用して例外メッセージを返します。
-
- public synchronized Throwable getCause() – このメソッドは、例外の原因を返します。原因が不明な場合はnullを返します。
-
- public String toString() – このメソッドは、Throwableの情報を文字列形式で返します。返される文字列には、Throwableクラスの名前とローカライズされたメッセージが含まれています。
- public void printStackTrace() – このメソッドはスタックトレース情報を標準エラーストリームに出力します。このメソッドはオーバーロードされており、スタックトレース情報をファイルまたはストリームに書き込むために引数としてPrintStreamまたはPrintWriterを渡すことができます。
Java 7では、自動リソース管理やキャッチブロックの改善が行われました。
1つのtryブロック内で多くの例外をキャッチしている場合、catchブロックのコードはエラーをログに記録するための冗長なコードで占められることに気付くでしょう。Java 7では、複数の例外を1つのcatchブロックでキャッチする改良された機能が導入されました。以下に、この機能を使用したcatchブロックの例を示します。
catch (IOException | SQLException ex) {
logger.error(ex);
throw new MyException(ex.getMessage());
}
例外オブジェクトはfinalであり、catchブロック内では修正できないなど、いくつかの制約があります。Java 7 Catch Block Improvementsの完全な解析はこちらをご覧ください。
ほとんどの場合、リソースをクローズするためにfinallyブロックを使用します。時にはそれを閉じるのを忘れ、リソースが枯渇した際に実行時例外が発生します。これらの例外はデバッグが難しく、そのリソースを使用している場所を見直してクローズしていることを確認する必要があります。Java 7では、try-with-resourcesが改善の一つとして導入されました。これにより、try文自体でリソースを作成し、try-catchブロックの中で使用することができます。try-catchブロックを抜けるときに、ランタイム環境が自動的にこれらのリソースをクローズします。以下は、この改善を利用したtry-catchブロックの例です。
try (MyResource mr = new MyResource()) {
System.out.println("MyResource created in try-with-resources");
} catch (Exception e) {
e.printStackTrace();
}
カスタム例外クラスの例
Javaは、私たちが使用するための多くの例外クラスを提供してくれますが、時には独自のカスタム例外クラスを作成する必要があります。たとえば、適切なメッセージで呼び出し元に特定の種類の例外を通知するためです。私たちは、エラーコードなどの追跡用のカスタムフィールドを持つことができます。例えば、テキストファイルのみを処理するメソッドを作成した場合、他の種類のファイルが入力された場合に、呼び出し元に適切なエラーコードを提供できます。
最初に、MyExceptionを作成してください。
package com.scdev.exceptions;
public class MyException extends Exception {
private static final long serialVersionUID = 4664456874499611218L;
private String errorCode = "Unknown_Exception";
public MyException(String message, String errorCode) {
super(message);
this.errorCode=errorCode;
}
public String getErrorCode() {
return this.errorCode;
}
}
その後、CustomExceptionExampleを作成してください。
package com.scdev.exceptions;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
public class CustomExceptionExample {
public static void main(String[] args) throws MyException {
try {
processFile("file.txt");
} catch (MyException e) {
processErrorCodes(e);
}
}
private static void processErrorCodes(MyException e) throws MyException {
switch (e.getErrorCode()) {
case "BAD_FILE_TYPE":
System.out.println("Bad File Type, notify user");
throw e;
case "FILE_NOT_FOUND_EXCEPTION":
System.out.println("File Not Found, notify user");
throw e;
case "FILE_CLOSE_EXCEPTION":
System.out.println("File Close failed, just log it.");
break;
default:
System.out.println("Unknown exception occured, lets log it for further debugging." + e.getMessage());
e.printStackTrace();
}
}
private static void processFile(String file) throws MyException {
InputStream fis = null;
try {
fis = new FileInputStream(file);
} catch (FileNotFoundException e) {
throw new MyException(e.getMessage(), "FILE_NOT_FOUND_EXCEPTION");
} finally {
try {
if (fis != null) fis.close();
} catch (IOException e) {
throw new MyException(e.getMessage(), "FILE_CLOSE_EXCEPTION");
}
}
}
}
私たちは、異なるメソッドから得る異なる種類のエラーコードを処理するために、別の方法を持つことができます。ユーザーに通知したくない場合、いくつかは消費されるかもしれませんが、問題をユーザーに通知するために、一部のエラーコードはスローバックします。
以下の文を日本語で表現します。一つのオプションでのみ提供します:
ここでは、Exceptionを拡張して、この例外が発生した場合には、メソッド内で処理されるか、呼び出し元プログラムに返される必要があります。RuntimeExceptionを拡張する場合は、throws節で明示する必要はありません。
これは設計上の決定でした。チェック済み例外を使用することで、開発者がどの例外が予想され、それらを適切に処理するための適切な対応をするのに役立ちます。
Javaでの例外処理のベストプラクティス
- Use Specific Exceptions – Base classes of Exception hierarchy don’t provide any useful information, that’s why Java has so many exception classes, such as IOException with further sub-classes as FileNotFoundException, EOFException, etc. We should always throw and catch specific exception classes so that caller will know the root cause of the exception easily and process them. This makes debugging easier and helps client applications handle exceptions appropriately.
- Throw Early or Fail-Fast – We should try to throw exceptions as early as possible. Consider the above processFile() method, if we pass the null argument to this method, we will get the following exception:
Exception in thread “main” java.lang.NullPointerException at java.io.FileInputStream.<init>(FileInputStream.java:134) at java.io.FileInputStream.<init>(FileInputStream.java:97) at com.scdev.exceptions.CustomExceptionExample.processFile(CustomExceptionExample.java:42) at com.scdev.exceptions.CustomExceptionExample.main(CustomExceptionExample.java:12)
デバッグ中は、例外の実際の発生場所を特定するためにスタックトレースを注意深く見る必要があります。以下のように実装ロジックを変更して、これらの例外を早期にチェックするようにします。
private static void processFile(String file) throws MyException {
if (file == null) throw new MyException("File name can't be null", "NULL_FILE_NAME");
// ... further processing
}
その後、例外のスタックトレースは、明確なメッセージと共に、例外が発生した場所を示します。
com.scdev.exceptions.MyException: File name can’t be null at com.scdev.exceptions.CustomExceptionExample.processFile(CustomExceptionExample.java:37) at com.scdev.exceptions.CustomExceptionExample.main(CustomExceptionExample.java:12)
- Catch Late – Since Java enforces to either handle the checked exception or to declare it in the method signature, sometimes developers tend to catch the exception and log the error. But this practice is harmful because the caller program doesn’t get any notification for the exception. We should catch exceptions only when we can handle them appropriately. For example, in the above method, I am throwing exceptions back to the caller method to handle it. The same method could be used by other applications that might want to process the exception in a different manner. While implementing any feature, we should always throw exceptions back to the caller and let them decide how to handle it.
- Closing Resources – Since exceptions halt the processing of the program, we should close all the resources in finally block or use Java 7 try-with-resources enhancement to let java runtime close it for you.
- Logging Exceptions – We should always log exception messages and while throwing exceptions provide a clear message so that caller will know easily why the exception occurred. We should always avoid an empty catch block that just consumes the exception and doesn’t provide any meaningful details of the exception for debugging.
- Single catch block for multiple exceptions – Most of the time we log exception details and provide a message to the user, in this case, we should use Java 7 feature for handling multiple exceptions in a single catch block. This approach will reduce our code size, and it will look cleaner too.
- Using Custom Exceptions – It’s always better to define an exception handling strategy at the design time and rather than throwing and catching multiple exceptions, we can create a custom exception with an error code, and the caller program can handle these error codes. It’s also a good idea to create a utility method to process different error codes and use them.
- Naming Conventions and Packaging – When you create your custom exception, make sure it ends with Exception so that it will be clear from the name itself that it’s an exception class. Also, make sure to package them like it’s done in the Java Development Kit (JDK). For example, IOException is the base exception for all IO operations.
- Use Exceptions Judiciously – Exceptions are costly, and sometimes it’s not required to throw exceptions at all, and we can return a boolean variable to the caller program to indicate whether an operation was successful or not. This is helpful where the operation is optional, and you don’t want your program to get stuck because it fails. For example, while updating the stock quotes in the database from a third-party web service, we may want to avoid throwing exceptions if the connection fails.
- Document the Exceptions Thrown – Use Javadoc @throws to clearly specify the exceptions thrown by the method. It’s very helpful when you are providing an interface for other applications to use.
結論
この記事では、Javaにおける例外処理について学びました。throwとthrowsについて学びました。また、try(およびtry-with-resources)、catch、finallyブロックについても学びました。