Javaでの例外処理の7つの一般的なエラーを修正します

こんにちは、Habr! Thorben JanssenによるFixing 7 Common Java Exception Handling Mistakesの翻訳を紹介します。



例外処理は最も一般的なものの1つですが、必ずしも最も単純なタスクの1つではありません。 これはまだ経験豊富なチームで頻繁に議論されているトピックの1つであり、注意すべきいくつかの良い習慣とよくある間違いがあります。



アプリケーションで例外を処理する際に避けるべきいくつかの事項を以下に示します。



エラー1:java.lang.Exceptionまたはjava.lang.Throwableの宣言



すでにご存じのとおり、チェック済み例外を宣言または処理する必要があります。 ただし、指定できる例外はチェック例外だけではありません。 throws句でjava.lang.Throwableの任意のサブクラスを使用できます。 したがって、次のコードフラグメントがスローする2つの異なる例外を指定する代わりに、throws句でjava.lang.Exceptionを使用できます。



public void doNotSpecifyException() throws Exception { doSomething(); } public void doSomething() throws NumberFormatException, IllegalArgumentException { // do something }
      
      





しかし、これはあなたがそうすべきだという意味ではありません。 ExeptionまたはThrowableを指定すると、メソッドを呼び出すときにそれらを正しく処理することがほぼ不可能になります。呼び出すメソッドが取得する唯一の情報は、何かが間違っている可能性があるということです。 ただし、発生する可能性のある例外的なイベントに関する情報は共有しません。 この情報を、例外をスローする一般的な理由の背後に隠し、アプリケーションが時間とともに変化するとさらに悪化します。 汎用例外をスローすると、呼び出し側が予期して処理する必要があるすべての例外の変更が隠されます。 これにより、コンパイラエラーではなく、テストケースで検出する必要のある予期しないエラーが発生する可能性があります。



特定のクラスを使用する



いくつかの例外クラスを使用する必要がある場合でも、最も具体的な例外クラスを指定することをお勧めします。 これは、どの例外イベントを処理するかを呼び出し元に伝えます。 また、メソッドが追加の例外をスローしたときにthrow句を更新することもできます。 したがって、顧客は変更を認識し、スローされた例外を変更するとエラーが発生します。 このような例外は、特定のテストケースを実行したときにのみ表示される例外よりも、見つけやすく、処理しやすいです。



 public void specifySpecificExceptions() throws NumberFormatException, IllegalArgumentException { doSomething(); }
      
      





間違い2:一般的な例外をキャッチする



このエラーの重大度は、実装しているソフトウェアコンポーネントと例外が発生した場所によって異なります。 Java SEアプリケーションのメインメソッドでjava.lang.Exceptionをキャッチすると便利です。 ただし、ライブラリを実装している場合、またはアプリケーションのより深い層で作業している場合は、特定の例外をキャッチすることをお勧めします。



これにはいくつかの利点があります。 このアプローチにより、例外の各クラスを異なる方法で処理でき、予期しない例外をキャッチすることはできません。



ただし、例外クラスまたはそのスーパークラスの1つを処理する最初のcatchブロックがそれをキャッチすることに注意してください。 したがって、最初に最も具体的なクラスをキャッチしてください。 そうしないと、IDEは到達不能なコードブロックに関するエラーまたは警告メッセージを表示します。



 try { doSomething(); } catch (NumberFormatException e) { // handle the NumberFormatException log.error(e); } catch (IllegalArgumentException e) { // handle the IllegalArgumentException log.error(e); }
      
      





エラー3:例外のログ記録とスロー



これは、Java例外を処理する際の最も一般的なエラーの1つです。 例外がスローされた場所に登録し、それを呼び出し元に転送することは論理的に思えるかもしれません。呼び出し元は、特定のユースケースに特定の処理を実装できます。 ただし、次の3つの理由でこれを行うべきではありません。



1.メソッドの呼び出しオブジェクトが実装したいユースケースに関する十分な情報がありません。 例外は予期される動作の一部であり、クライアントによって処理される場合があります。 この場合、登録する必要はありません。 これにより、ログファイルに誤ったエラーメッセージが追加されます。これは、オペレーティンググループによってフィルタリングする必要があります。



2.ジャーナルメッセージは、まだ例外自体の一部ではない情報を提供しません。 そのトレースとスタックトレースには、例外イベントに関する必要な情報がすべて含まれている必要があります。 メッセージはこれを説明し、スタックトレースには、発生したクラス、メソッド、および行に関する詳細情報が含まれます。



3.キャッチするすべてのcatchブロックで同じ例外を登録すると、同じ例外を数回キャッチできます。 これにより、監視ツールの統計が台無しになり、運用チームと開発チームのログファイルを読みにくくなります。



例外を処理する場所に記録します



したがって、例外を処理しているときにキャッチすることをお勧めします。 次のコードスニペットのように。 doSomethingメソッドは例外をスローします。 開発者がそれを処理するのに十分な情報を持っていないので、doMoreメソッドは単にそれを指摘します。 その後、doEvenMoreメソッドで処理され、ログメッセージも書き込まれます。



 public void doEvenMore() { try { doMore(); } catch (NumberFormatException e) { // handle the NumberFormatException } catch (IllegalArgumentException e) { // handle the IllegalArgumentException } } public void doMore() throws NumberFormatException, IllegalArgumentException { doSomething(); } public void doSomething() throws NumberFormatException, IllegalArgumentException { // do something }
      
      





間違い4:例外を使用してフローを制御する



例外を使用してアプリケーションのフローを制御することは、2つの主な理由からアンチパターンと見なされます。



コードブロックの実行をキャンセルし、例外を処理する最初のcatchブロックに移動するため、基本的にGo Toステートメントとして機能します。 これにより、コードが非常に読みにくくなります。



一般的なJava管理構造ほど効果的ではありません。 名前が示すように、例外イベントにのみ使用する必要があり、JVMは他のコードと同じように最適化しないため、ループまたはif-elseステートメントを中断して適切なブロックを決定することをお勧めしますコードを実行する必要があります。



エラー5:例外の原因を取り除く



ある例外を別の例外にラップする必要がある場合があります。 おそらく、あなたのチームは、エラーコードと単一の処理を伴うビジネスのために特別な例外を使用することを決定しました。 原因に対処しない限り、このアプローチには何の問題もありません。



新しい例外を作成するときは、常に元の例外を理由として設定する必要があります。 そうしないと、例外の原因となった例外イベントを説明するメッセージとスタックのトレースが失われます。 Exceptionクラスとそのすべてのサブクラスは、元の例外をパラメーターとして受け取り、それを原因として設定するいくつかのコンストラクターメソッドを提供します。



 try { doSomething(); } catch (NumberFormatException e) { throw new MyBusinessException(e, ErrorCode.CONFIGURATION_ERROR); } catch (IllegalArgumentException e) { throw new MyBusinessException(e, ErrorCode.UNEXPECTED); }
      
      





エラー6:例外を一般化する



例外を一般化すると、NumberFormatExceptionなどの特定の例外をキャッチし、代わりに非特定のjava.lang.Exceptionをスローします。 これは似ていますが、この記事で説明した最初のエラーよりもさらに悪いものです。 APIのエラーの特定のケースに関する情報を隠すだけでなく、アクセスを困難にします。



 public void doNotGeneralizeException() throws Exception { try { doSomething(); } catch (NumberFormatException e) { throw new Exception(e); } catch (IllegalArgumentException e) { throw new Exception(e); } }
      
      





次のコードスニペットで見ることができるように、メソッドが引き起こす例外を知っていても、それらを単にキャッチすることはできません。 汎用のExceptionクラスをキャッチして、その原因のタイプを確認する必要があります。 このコードは実装が面倒であるだけでなく、読みにくいものです。 このアプローチとエラー5を組み合わせるとさらに悪化します。これにより、例外イベントに関するすべての情報が削除されます。



 try { doNotGeneralizeException(); } catch (Exception e) { if (e.getCause() instanceof NumberFormatException) { log.error("NumberFormatException: " + e); } else if (e.getCause() instanceof IllegalArgumentException) { log.error("IllegalArgumentException: " + e); } else { log.error("Unexpected exception: " + e); } }
      
      





それでは、最善のアプローチは何ですか?



特定し、例外の原因を保持します。



スローする例外は、できる限り具体的にする必要があります。 また、例外をラップする場合は、スタックトレースや例外イベントを説明するその他の情報が失われないように、元の例外も理由として設定する必要があります。



 try { doSomething(); } catch (NumberFormatException e) { throw new MyBusinessException(e, ErrorCode.CONFIGURATION_ERROR); } catch (IllegalArgumentException e) { throw new MyBusinessException(e, ErrorCode.UNEXPECTED); }
      
      





エラー7:不要な例外変換を追加する



前に説明したように、元の例外を理由として設定した場合、カスタム例外をラップすると便利です。 しかし、一部のアーキテクトはそれをやりすぎて、アーキテクチャレベルごとに特別なクラスの例外を導入しています。 したがって、永続レベルで例外をキャッチし、それをMyPersistenceExceptionに転送します。 ビジネス層はそれをキャッチしてMyBusinessExceptionにラップし、これはAPIレベルに達するか処理されるまで続きます。



 public void persistCustomer(Customer c) throws MyPersistenceException { // persist a Customer } public void manageCustomer(Customer c) throws MyBusinessException { // manage a Customer try { persistCustomer(c); } catch (MyPersistenceException e) { throw new MyBusinessException(e, e.getCode()); } } public void createCustomer(Customer c) throws MyApiException { // create a Customer try { manageCustomer(c); } catch (MyBusinessException e) { throw new MyApiException(e, e.getCode()); } }
      
      





これらの追加の例外クラスには何の利点もありません。 単に例外をラップする追加のレイヤーを導入するだけです。 そして、贈り物をたくさんのカラフルな紙で包むのは楽しいだろうが、これはソフトウェア開発への良いアプローチではない。



情報を追加してください



例外を処理するコードについて、または例外の原因となった問題を見つける必要があるときに自分自身について考えてください。 最初に、いくつかの例外レベルを突破して根本原因を見つける必要があります。 そして今日まで、私はこのアプローチを使用したアプリケーションを見たことがなく、各例外レイヤーに有用な情報を追加しました。 エラーメッセージとコードを要約するか、冗長な情報を提供します。



したがって、導入するカスタム例外クラスの数に注意してください。 新しい例外クラスが追加情報やその他の利点を提供するかどうかを常に自問する必要があります。 ほとんどの場合、これを達成するために複数レベルのカスタム例外は必要ありません。



 public void persistCustomer(Customer c) { // persist a Customer } public void manageCustomer(Customer c) throws MyBusinessException { // manage a Customer throw new MyBusinessException(e, e.getCode()); } public void createCustomer(Customer c) throws MyBusinessException { // create a Customer manageCustomer(c); }
      
      






All Articles