一般的な例外の危険性に注意してください

最近のプロジェクトに取り組んでいるときに、リソースのクリーンアップを実行するコードを見つけました。さまざまな呼び出しがあったため、6つの異なる例外がスローされる可能性がありました。元のプログラマーは、コードを単純化する(または単に入力を保存する)ために、スローさExceptionれる可能性のある6つの異なる例外ではなく、メソッドがスローすることを宣言しました。これにより、呼び出し元のコードは、キャッチしたtry / catchブロックにラップされましたException。プログラマーは、コードがクリーンアップを目的としているため、失敗のケースは重要ではないと判断しました。そのため、システムがシャットダウンしても、catchブロックは空のままでした。

明らかに、これらは最良のプログラミング手法ではありませんが、ひどく間違っているものは何もないようです...元のコードの3行目の小さなロジックの問題を除いて:

リスト1.元のクリーンアップコード

private void cleanupConnections()throws ExceptionOne、ExceptionTwo {for(int i = 0; i <connections.length; i ++){connection [i] .release(); // ExceptionOne、ExceptionTwoをスローしますconnection [i] = null; }接続= null; } protected abstract void cleanupFiles()throws ExceptionThree、ExceptionFour; protected abstract void removeListeners()はExceptionFive、ExceptionSixをスローします。public void cleanupEverything()は例外をスローします{cleanupConnections(); cleanupFiles(); removeListeners(); } public void done(){try {doStuff(); cleanupEverything(); doMoreStuff(); } catch(例外e){}}

コードの別の部分ではconnections、最初の接続が作成されるまで配列は初期化されません。ただし、接続が作成されない場合、接続配列はnullになります。したがって、場合によっては、を呼び出すとconnections[i].release()結果がになりNullPointerExceptionます。これは比較的簡単に修正できる問題です。のチェックを追加するだけですconnections != null

ただし、例外が報告されることはありません。によってcleanupConnections()スローされcleanupEverything()、によって再びスローされ、最終的ににキャッチされdone()ます。このdone()メソッドは例外を除いて何もしません。ログにも記録しません。また、cleanupEverything()が呼び出されるだけなのでdone()、例外は発生しません。したがって、コードが修正されることはありません。

したがって、障害シナリオではcleanupFiles()removeListeners()メソッドとメソッドが呼び出されることはなく(したがって、リソースが解放さdoMoreStuff()れることもありません)、呼び出されるdone()こともありません。したがって、の最終処理が完了することはありません。さらに悪いことdone()に、システムがシャットダウンしたときに呼び出されません。代わりに、すべてのトランザクションを完了するために呼び出されます。そのため、すべてのトランザクションでリソースがリークします。

この問題は明らかに大きな問題です。エラーは報告されず、リソースがリークします。しかし、コード自体はかなり無実のようであり、コードの記述方法からすると、この問題を追跡するのは困難です。ただし、いくつかの簡単なガイドラインを適用することで、問題を見つけて修正できます。

  • 例外を無視しないでください
  • 一般的なキャッチしないでくださいException
  • ジェネリック捨ててはいけないExceptionのを

例外を無視しないでください

リスト1のコードの最も明らかな問題は、プログラムのエラーが完全に無視されていることです。予期しない例外(その性質上、予期しない例外)がスローされており、コードはその例外を処理する準備ができていません。コードは予期された例外が影響を及ぼさないと想定しているため、例外は報告されません。

ほとんどの場合、少なくとも例外をログに記録する必要があります。いくつかのロギングパッケージ(補足記事「ロギング例外」を参照)は、システムパフォーマンスに大きな影響を与えることなく、システムエラーと例外をログに記録できます。ほとんどのロギングシステムでは、スタックトレースを印刷することもできるため、例外が発生した場所と理由に関する貴重な情報が提供されます。最後に、ログは通常ファイルに書き込まれるため、例外の記録を確認および分析できます。スタックトレースのログ記録の例については、サイドバーのリスト11を参照してください。

いくつかの特定の状況では、例外のロギングは重要ではありません。これらの1つは、finally節のリソースのクリーニングです。

ついに例外

リスト2では、一部のデータがファイルから読み取られます。例外がデータを読み取るかどうかに関係なくファイルを閉じる必要があるため、close()メソッドはfinally句でラップされます。しかし、エラーがファイルを閉じる場合、それについては多くのことを行うことができません。

リスト2

public void loadFile(String fileName)throws IOException {InputStream in = null; {in = new FileInputStream(fileName);を試してください。readSomeData(in); }最後に{if(in!= null){try {in.close(); } catch(IOException ioe){//無視}}}}

I / O(入力/出力)の問題が原因で実際​​のデータのロードが失敗した場合loadFile()でもIOException、呼び出し元のメソッドにを報告することに注意してください。また、からの例外close()は無視されますが、コードで作業しているすべての人に明確にするために、コードにはコメントで明示的に記載されていることに注意してください。これと同じ手順を、すべてのI / Oストリームのクリーンアップ、ソケットとJDBC接続のクローズなどに適用できます。

例外を無視することについて重要なことは、単一のメソッドのみが無視するtry / catchブロックでラップされ(したがって、囲んでいるブロック内の他のメソッドが引き続き呼び出される)、特定の例外がキャッチされるようにすることです。この特別な状況は、ジェネリックをキャッチすることとは明らかに異なりExceptionます。他のすべての場合、例外は(少なくとも)ログに記録する必要があります。できればスタックトレースを使用してください。

一般的な例外をキャッチしないでください

多くの場合、複雑なソフトウェアでは、特定のコードブロックが、さまざまな例外をスローするメソッドを実行します。動的にクラスをロードし、オブジェクトをインスタンス化することを含む、いくつかの異なる例外を投げることができるClassNotFoundExceptionInstantiationExceptionIllegalAccessException、とClassCastException

4つの異なるcatchブロックをtryブロックに追加する代わりに、忙しいプログラマーは、メソッド呼び出しを、ジェネリックをキャッチするtry / catchブロックでラップするだけですException(以下のリスト3を参照)。これは無害に見えますが、意図しない副作用が発生する可能性があります。たとえば、className()がnullの場合、Class.forName()がスローNullPointerExceptionされ、メソッドでキャッチされます。

その場合には、catchブロックは、例外ので、それがキャッチする意図はありませんキャッチNullPointerExceptionのサブクラスであるRuntimeException今度は、のサブクラスです、、 Exception。したがって、ジェネリックはcatch (Exception e)、、、およびRuntimeExceptionを含むNullPointerException、のすべてのサブクラスをキャッチします。通常、プログラマーはこれらの例外をキャッチするつもりはありません。IndexOutOfBoundsExceptionArrayStoreException

リスト3では、null className結果NullPointerExceptionはになります。これは、呼び出し元のメソッドにクラス名が無効であることを示します。

リスト3

public SomeInterface buildInstance(String className){SomeInterface impl = null; {Class clazz = Class.forName(className);を試してください。impl =(SomeInterface)clazz.newInstance(); } catch(Exception e){log.error( "クラスの作成中にエラーが発生しました:" + className); } return impl; }

一般的なcatch句の別の結果は、キャッチされcatchている特定の例外がわからないため、ロギングが制限されることです。一部のプログラマーは、この問題に直面したときに、例外タイプを確認するためのチェックを追加することに頼ります(リスト4を参照)。これは、catchブロックを使用する目的と矛盾します。

リスト4

catch(Exception e){if(e instanceof ClassNotFoundException){log.error( "Invalid class name:" + className + "、" + e.toString()); } else {log.error( "クラスを作成できません:" + className + "、" + e.toString()); }}

リスト5は、プログラマーが関心を持つ可能性のある特定の例外をキャッチする完全な例を示しています。instanceof特定の例外がキャッチされるため、オペレーターは必要ありません。チェック例外の各(ClassNotFoundExceptionInstantiationExceptionIllegalAccessException)がキャッチして扱われます。ClassCastException(クラスは正しくロードされますが、SomeInterfaceインターフェイスは実装されません)を生成する特殊なケースも、その例外をチェックすることによって検証されます。

リスト5

public SomeInterface buildInstance(String className) { SomeInterface impl = null; try { Class clazz = Class.forName(className); impl = (SomeInterface)clazz.newInstance(); } catch (ClassNotFoundException e) { log.error("Invalid class name: " + className + ", " + e.toString()); } catch (InstantiationException e) { log.error("Cannot create class: " + className + ", " + e.toString()); } catch (IllegalAccessException e) { log.error("Cannot create class: " + className + ", " + e.toString()); } catch (ClassCastException e) { log.error("Invalid class type, " + className + " does not implement " + SomeInterface.class.getName()); } return impl; } 

In some cases, it is preferable to rethrow a known exception (or perhaps create a new exception) than try to deal with it in the method. This allows the calling method to handle the error condition by putting the exception into a known context.

Listing 6 below provides an alternate version of the buildInterface() method, which throws a ClassNotFoundException if a problem occurs while loading and instantiating the class. In this example, the calling method is assured to receive either a properly instantiated object or an exception. Thus, the calling method does not need to check if the returned object is null.

Note that this example uses the Java 1.4 method of creating a new exception wrapped around another exception to preserve the original stack trace information. Otherwise, the stack trace would indicate the method buildInstance() as the method where the exception originated, instead of the underlying exception thrown by newInstance():

Listing 6

public SomeInterface buildInstance(String className) throws ClassNotFoundException { try { Class clazz = Class.forName(className); return (SomeInterface)clazz.newInstance(); } catch (ClassNotFoundException e) { log.error("Invalid class name: " + className + ", " + e.toString()); throw e; } catch (InstantiationException e) { throw new ClassNotFoundException("Cannot create class: " + className, e); } catch (IllegalAccessException e) { throw new ClassNotFoundException("Cannot create class: " + className, e); } catch (ClassCastException e) { throw new ClassNotFoundException(className + " does not implement " + SomeInterface.class.getName(), e); } } 

In some cases, the code may be able to recover from certain error conditions. In these cases, catching specific exceptions is important so the code can figure out whether a condition is recoverable. Look at the class instantiation example in Listing 6 with this in mind.

In Listing 7, the code returns a default object for an invalid className, but throws an exception for illegal operations, like an invalid cast or a security violation.

Note:IllegalClassException is a domain exception class mentioned here for demonstration purposes.

Listing 7

public SomeInterface buildInstance(String className) throws IllegalClassException { SomeInterface impl = null; try { Class clazz = Class.forName(className); return (SomeInterface)clazz.newInstance(); } catch (ClassNotFoundException e) { log.warn("Invalid class name: " + className + ", using default"); } catch (InstantiationException e) { log.warn("Invalid class name: " + className + ", using default"); } catch (IllegalAccessException e) { throw new IllegalClassException("Cannot create class: " + className, e); } catch (ClassCastException e) { throw new IllegalClassException(className + " does not implement " + SomeInterface.class.getName(), e); } if (impl == null) { impl = new DefaultImplemantation(); } return impl; } 

When generic Exceptions should be caught

Certain cases justify when it is handy, and required, to catch generic Exceptions. These cases are very specific, but important to large, failure-tolerant systems. In Listing 8, requests are read from a queue of requests and processed in order. But if any exceptions occur while the request is being processed (either a BadRequestException or any subclass of RuntimeException, including NullPointerException), then that exception will be caught outside the processing while loop. So any error causes the processing loop to stop, and any remaining requests will not be processed. That represents a poor way of handling an error during request processing:

Listing 8

public void processAllRequests(){Request req = null; {while(true){req = getNextRequest();を試してください。if(req!= null){processRequest(req); // BadRequestExceptionをスローします} else {//リクエストキューは空です。break;を実行する必要があります。}}} catch(BadRequestException e){log.error( "Invalid request:" + req、e); }}