Javaで例外を処理するのは簡単なことではありません。初心者には理解しにくいだけでなく、経験豊富な開発者であっても、どの例外をどのように処理するかなど、例外の処理方法について多くの時間をかけて考える必要があります。そのため、ほとんどの開発チームでは例外処理を規定するルールを設けています。これらのルールは、チームによって大きく異なることがよくあります。
この記事では、多くのチームで使われている例外処理のベストプラクティスをいくつか紹介します。
Finallyブロックでリソースをクリーンアップするか、try-with-resource文を使用します。
InputStreamのように使用後に閉じる必要があるリソースを使用する場合、よくある間違いはtryブロックの最後でリソースを閉じてしまうことです。
public void doNotCloseResourceInTry() { FileInputStream inputStream = null; try { File file = new File("./tmp.txt"); inputStream = new FileInputStream(file); // use the inputStream to read a file // do NOT do this inputStream.close(); } catch (FileNotFoundException e) { log.error(e); } catch (IOException e) { log.error(e); }}
上記のコードは例外が発生することなく正常に実行されます。しかし、tryブロック内のステートメントが例外をスローしたり、独自に実装したコードが例外をスローした場合、最後のcloseステートメントは実行されず、リソースは解放されません。
すべてのクリーンアップコードをfinallyブロックに入れるか、try-with-resource文を使うのが理にかなっています。
public void closeResourceInFinally() { FileInputStream inputStream = null; try { File file = new File("./tmp.txt"); inputStream = new FileInputStream(file); // use the inputStream to read a file } catch (FileNotFoundException e) { log.error(e); } finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { log.error(e); } } }}public void automaticallyCloseResource() { File file = new File("./tmp.txt"); try (FileInputStream inputStream = new FileInputStream(file);) { // use the inputStream to read a file } catch (FileNotFoundException e) { log.error(e); } catch (IOException e) { log.error(e); }}
特定の例外の指定
コードを理解しやすくするために、可能な限り具体的な例外を使用してメソッドを宣言します。
public void doNotDoThis() throws Exception { ...}public void doThis() throws NumberFormatException { ...}
上記のように、NumberFormatException は文字通り数値のフォーマットエラーです。
例外の文書化
メソッドで例外のスローを宣言する場合にも、文書化が必要です。前述と同様、例外を回避/処理しやすくするために、呼び出し側にできるだけ多くの情報を与えることです。例外処理のための10のベストプラクティス、こちらも一読をお勧めします。
Javadocにthrows文を追加し、例外がスローされるシナリオを記述します。
/** * This method does something extremely useful ... * * @param input * @throws MyBusinessException if ... happens */public void doSomething(String input) throws MyBusinessException { ...}
例外をスローするときは、説明的な情報を含めます。
例外を発生させるときは、問題や関連情報をできるだけ正確に記述し、ログや監視ツールに出力することで、特定のエラーメッセージやエラーの深刻度などをより簡単に読み取れるようにする必要があります。
しかし、これはエラーメッセージを長々と説明するためのものではなく、Exceptionというクラス名がエラーの原因を反映しているはずなので、1つか2つの文章で説明すればよいのです。
try { new Long("xyz");} catch (NumberFormatException e) { log.error(e);}
NumberFormatException は、例外がフォーマット・エラーであることを伝え、例外の追加情報は単にこのエラー文字列を提供するだけです。例外の名前が明白でない場合は、できるだけ具体的な情報を提供する必要があります。
最も特殊な例外を最初にキャッチ
多くのIDEは、最も一般的な例外を最初にキャッチしようとすると、到達不可能なコードを示唆することで、このベストプラクティスをインテリジェントに示唆するようになりました。catch ブロックが複数ある場合、catch の順番は catch ブロックに最初にマッチしたものだけが実行されます。したがって、IllegalArgumentException が最初にキャッチされた場合、NumberFormatException のキャッチは実行できません。
public void catchMostSpecificExceptionFirst() { try { doSomething("A message"); } catch (NumberFormatException e) { log.error(e); } catch (IllegalArgumentException e) { log.error(e) }}
Throwableをキャッチしない
Throwableはすべての例外とエラーの親クラスです。catch 文で捕捉することはできますが、決して捕捉しないでください。throwableをキャッチすると、すべての例外をキャッチするだけでなく、エラーもキャッチすることになります。エラーは、回復できないことを示すjvmエラーです。そのため、絶対に処理できるという確信があるか、エラーを処理する必要がある場合を除き、throwableをキャッチしてはいけません。
public void doNotCatchThrowable() { try { // do something } catch (Throwable t) { // don't do this! }}
例外を無視しないでください
開発者は、例外がスローされないと確信するあまり、キャッチブロックを書いても処理やロギングを行わないことがよくあります。
public void doNotIgnoreExceptions() { try { // do something } catch (NumberFormatException e) { // this will never happen }}
しかし現実には、予期せぬ例外が発生したり、ここのコードが将来変更されるかどうかが不明確だったりすることが多く、例外がキャッチされたことで、問題を特定するのに十分なエラー情報を得ることができなくなってしまいます。少なくとも例外をログに記録することには意味があります。
public void logAnException() { try { // do something } catch (NumberFormatException e) { log.error("This should never happen: " + e); }}
ログに記録せず、例外をスローしない
例外をキャッチし、ログに記録し、再び例外をスローするロジックを持つコードやクラス・ライブラリがたくさんあります。以下はその例です:
try { new Long("xyz");} catch (NumberFormatException e) { log.error(e); throw e;}
この処理ロジックは合理的に見えます。しかし、これはしばしば同じ例外に対して複数のログを出力します。以下はその例です:
17:44:28,945 ERROR TestExceptionHandling:65 - java.lang.NumberFormatException: For input string: "xyz"Exception in thread "main" java.lang.NumberFormatException: For input string: "xyz"at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)at java.lang.Long.parseLong(Long.java:589)at java.lang.Long.(Long.java:965)at com.stackify.example.TestExceptionHandling.logAndThrowException(TestExceptionHandling.java:63)at com.stackify.example.TestExceptionHandling.main(TestExceptionHandling.java:58)
上で見たように、後のログにはそれ以上有用な情報はありません。より有用な情報が必要な場合は、例外をカスタム例外としてラップすることができます。
public void wrapException(String input) throws MyBusinessException { try { // do something } catch (NumberFormatException e) { throw new MyBusinessException("A message that describes the error.", e); }}
したがって、例外をキャッチするのは例外を処理したいときだけで、そうでない場合はメソッドのシグネチャで例外を宣言し、呼び出し元が処理できるようにします。
例外のラップ時に元の例外を破棄しない
標準的な例外をキャッチし、それをカスタム例外としてラップするのが一般的です。こうすることで、より具体的な例外情報を追加し、的を絞った例外処理ができるようになります。
例外をラップする場合は、必ず元の例外を cause に設定してください(Exception には cause を渡すコンストラクタ・メソッドがあります)。そうしないと、元の例外情報が失われ、エラー解析が困難になります。
public void wrapException(String input) throws MyBusinessException { try { // do something } catch (NumberFormatException e) { throw new MyBusinessException("A message that describes the error.", e); }}
まとめ
上記からわかるように、例外をスローしたりキャッチしたりする際に考慮すべき点は多岐にわたります。これらの点の多くは、コードの読みやすさやapiの使いやすさを向上させるためのものです。例外は単なるエラー制御の仕組みではなく、コミュニケーションの手段でもあります。そのため、これらのベストプラクティスを共同作業者と話し合い、いくつかの仕様を策定することで、誰もが共通の概念を理解し、同じように使用できるようになります。





