Java Language
Javaの落とし穴 - 例外の使用
サーチ…
前書き
いくつかのJavaプログラミング言語の誤用は、正しくコンパイルされているにもかかわらず不正な結果を生成するプログラムを実行する可能性があります。このトピックの主な目的は、 例外処理に関連する一般的な落とし穴をリストし、そのような落とし穴を避けるための正しい方法を提案することです。
落とし穴 - 例外を無視または潰す
この例では、意図的に例外を無視するか、または「スカッシュ」します。あるいは、より正確に言えば、それを無視して例外を捕捉して処理する方法です。しかし、これを行う方法を説明する前に、まず例外を押しつぶすことは、一般的にそれらを処理する正しい方法ではないことを指摘しておきます。
例外は、プログラムの他の部分にいくつかの重要な(すなわち「例外的な」)イベントが発生したことを通知するために(何かによって)スローされます。一般的に(いつもとは限りませんが)、例外は何かが間違っていることを意味します。例外をスクラッシュするようにプログラムをコーディングすると、問題が別の形式で再び現れる可能性があります。状況を悪化させるために、例外をスカッシュすると、例外オブジェクト内の情報とそれに関連するスタックトレースがスローされます。それは、問題の元の原因が何であるか把握することを困難にする可能性があります。
実際には、IDEの自動修正機能を使用して未処理の例外によって発生したコンパイルエラーを「修正」すると、例外が頻繁に発生します。たとえば、次のようなコードが表示されます。
try {
inputStream = new FileInputStream("someFile");
} catch (IOException e) {
/* add exception handling code here */
}
明らかに、プログラマはコンパイルエラーを消すためにIDEの提案を受け入れましたが、提案は不適切でした。 (上記の "訂正"を行うと、プログラムが後で失敗する可能性があります(例えば、 inputStream
がnull
ためにNullPointerException
したNullPointerException
)。
言いましたが、意図的に例外を押しつぶす例があります。 (議論の目的のために、私たちがセルフリーダーを見せている間に割り込みが無害であると判断したと仮定してください)。このコメントは、例外的に押しつぶしたことを読者に伝えています。
try {
selfie.show();
} catch (InterruptedException e) {
// It doesn't matter if showing the selfie is interrupted.
}
例外的な変数の名前をつけてこれを示すのはどういう意味なのか、 意図的に例外を押しつぶしていることを強調する別の従来の方法です:
try {
selfie.show();
} catch (InterruptedException ignored) { }
変数名がignored
設定さignored
いると、一部のIDE(IntelliJ IDEAなど)には空のキャッチブロックに関する警告が表示されません。
落とし穴 - 例外、例外、エラーまたはRuntimeExceptionの捕捉
経験の浅いJavaプログラマーの共通の考え方は、例外は「問題」または「負担」であり、これを処理する最善の方法はできるだけ早くそれらをすべて1つにすることです。これは次のようなコードにつながります:
....
try {
InputStream is = new FileInputStream(fileName);
// process the input
} catch (Exception ex) {
System.out.println("Could not open file " + fileName);
}
上記のコードには重大な欠陥があります。 catch
は実際にはプログラマが期待しているより多くの例外をキャッチしようとしています。アプリケーションの他の場所にあるバグのために、 fileName
の値がnull
であるとします。これにより、 FileInputStream
コンストラクターはNullPointerException
をスローします。ハンドラはこれをキャッチし、ユーザに報告します:
Could not open file null
これは役に立たず、混乱します。さらに悪いことに、予期しない例外(チェックされているかチェックされていない!)を投げたのは、それが "入力を処理する"コードだとします。これで、ユーザーは、ファイルを開くときに発生しなかった問題の誤解を招くメッセージを受け取り、I / Oにはまったく関係しなくなる可能性があります。
問題の根本は、プログラマがException
ハンドラをコード化していることです。これはほとんどの場合間違いです。
- Catching
Exception
はチェックされたすべての例外とチェックされていない例外も捕捉します。 -
RuntimeException
をキャッチすると、ほとんどチェックされていない例外がキャッチされます。 - Catching
Error
は、JVMの内部エラーを通知する未チェックの例外を捕捉します。これらのエラーは一般的に回復不可能であり、捕まえるべきではありません。 - catch
Throwable
はすべての例外をキャッチします。
あまりにも広範な例外をキャッチする問題は、ハンドラが通常それらのすべてを適切に処理できないことです。 Exception
などの場合、プログラマが何が捕捉されるかを予測することは困難です。すなわち、何を期待するか。
一般的に、適切な解決策はスローされた例外を処理することです 。たとえば、それらをキャッチしてその場で扱うことができます。
try {
InputStream is = new FileInputStream(fileName);
// process the input
} catch (FileNotFoundException ex) {
System.out.println("Could not open file " + fileName);
}
囲む方法でthrown
して宣言することもできます。
Exception
キャッチするのが適切な状況はほとんどありません。一般的に発生する唯一のものは、次のようなものです。
public static void main(String[] args) {
try {
// do stuff
} catch (Exception ex) {
System.err.println("Unfortunately an error has occurred. " +
"Please report this to X Y Z");
// Write stacktrace to a log file.
System.exit(1);
}
}
ここではすべての例外を真に処理したいので、 Exception
(またはThrowable
)をキャッチするのは正しいです。
落とし穴 - Throwable、Exception、ErrorまたはRuntimeExceptionを投げる
Throwable
、 Exception
、 Error
、およびRuntimeException
例外を捕捉するのは悪いですが、投げることはさらに悪いことです。
基本的な問題は、アプリケーションで例外を処理する必要があるときに、トップレベルの例外が存在すると、さまざまなエラー状態を区別することが困難になることです。例えば
try {
InputStream is = new FileInputStream(someFile); // could throw IOException
...
if (somethingBad) {
throw new Exception(); // WRONG
}
} catch (IOException ex) {
System.err.println("cannot open ...");
} catch (Exception ex) {
System.err.println("something bad happened"); // WRONG
}
問題は、 Exception
インスタンスを投げたので、強制的にそれをキャッチすることです。しかし、別の例で説明したように、 Exception
キャッチするのは悪いです。この状況では、 somethingBad
がtrue
場合にスローされるException
「予想される」ケースと、 NullPointerException
などの未チェックの例外を実際にキャッチする予期しないケースを区別することが困難になります。
最上位の例外が伝播することが許可されている場合は、他の問題が発生します。
- 私たちは今、トップレベルを投げ捨て、それを差別化/扱うさまざまな理由のすべてを覚えなければなりません。
-
Exception
とThrowable
場合、例外を伝播させたい場合は、これらの例外をメソッドのthrows
節に追加する必要もあります。これは下記のように問題があります。
つまり、これらの例外をスローしないでください。起こった "例外的な出来事"をより詳細に説明する、より具体的な例外を投げてください。必要な場合は、カスタム例外クラスを定義して使用します。
メソッドの "throws"でThrowableまたはExceptionを宣言するのは問題があります。
メソッドのthrows
節でthrows
れた例外の長いリストをException
またはThrowableに置き換えることは魅力的です。これは悪い考えです:
- これは、呼び出し元に
Exception
を処理(または伝播)させException
。 - 私たちはもはや、処理する必要がある特定のチェック例外について私たちに教えるためにコンパイラに頼ることはできません。
-
Exception
正しく処理するのは難しいです。実際の例外がどのように捕捉されるのかを知るのは難しく、何が捕まえることができないのかわからない場合、回復戦略が適切であるかどうかを知ることは難しいです。 -
Throwable
処理はさらに難しくなります。なぜなら、決して回復できない可能性のある障害に対処しなければならないからです。
このアドバイスは、他のパターンを避けるべきことを意味します。例えば:
try {
doSomething();
} catch (Exception ex) {
report(ex);
throw ex;
}
上記では、すべての例外を確実に処理せずにすべての例外をログに記録しようとしています。残念ながら、Java 7の前には、 throw ex;
ステートメントによって、コンパイラーはException
がスローされると考えるようになりました。これは、囲みメソッドをthrows Exception
として宣言するよう強制する可能性throws Exception
。 Java 7以降では、コンパイラーは、(再スローされる)例外セットがより小さくなることを知っています。
落とし穴 - InterruptedExceptionをキャッチする
既に他の落とし穴で指摘されているように、
try {
// Some code
} catch (Exception) {
// Some error handling
}
さまざまな問題がたくさんあります。しかし、1つのperticular問題は、マルチスレッドアプリケーションを書くときに割り込みシステムを破壊するので、デッドロックにつながる可能性があるということです。
スレッドを開始する場合は、通常、さまざまな理由で突然停止する必要があります。
Thread t = new Thread(new Runnable() {
public void run() {
while (true) {
//Do something indefinetely
}
}
}
t.start();
//Do something else
// The thread should be canceld if it is still active.
// A Better way to solve this is with a shared variable that is tested
// regularily by the thread for a clean exit, but for this example we try to
// forcibly interrupt this thread.
if (t.isAlive()) {
t.interrupt();
t.join();
}
//Continue with program
t.interrupt()
はスレッドをシャットダウンするよりも、そのスレッドでInterruptedExceptionを発生させます。しかし、スレッドが完全に停止する前にいくつかのリソースをクリーンアップする必要がある場合はどうしますか?このため、InterruptedExceptionを捕捉してクリーンアップを行うことができます。
Thread t = new Thread(new Runnable() {
public void run() {
try {
while (true) {
//Do something indefinetely
}
} catch (InterruptedException ex) {
//Do some quick cleanup
// In this case a simple return would do.
// But if you are not 100% sure that the thread ends after
// catching the InterruptedException you will need to raise another
// one for the layers surrounding this code.
Thread.currentThread().interrupt();
}
}
}
しかし、コード内にcatch-all式がある場合、InterruptedExceptionも同様にキャッチされ、中断は継続されません。親スレッドがt.join()
停止するのを無期限に待つため、この場合はデッドロックになる可能性があります。
Thread t = new Thread(new Runnable() {
public void run() {
try {
while (true) {
try {
//Do something indefinetely
}
catch (Exception ex) {
ex.printStackTrace();
}
}
} catch (InterruptedException ex) {
// Dead code as the interrupt exception was already caught in
// the inner try-catch
Thread.currentThread().interrupt();
}
}
}
したがって、個々に例外を捕捉する方が良いですが、catch-allを使用することを強く求めている場合は、少なくとも事前にInterruptedExceptionを個別に捕捉してください。
Thread t = new Thread(new Runnable() {
public void run() {
try {
while (true) {
try {
//Do something indefinetely
} catch (InterruptedException ex) {
throw ex; //Send it up in the chain
} catch (Exception ex) {
ex.printStackTrace();
}
}
} catch (InterruptedException ex) {
// Some quick cleanup code
Thread.currentThread().interrupt();
}
}
}
落とし穴 - 通常のフローコントロールに対する例外の使用
Javaの専門家の中には、
「例外は、例外的な場合にのみ使用するべきです。
(例: http : //programmers.stackexchange.com/questions/184654 )
これの本質は、例外や例外処理を使って通常のフロー制御を実装するのは悪い考えです(Javaの場合)。たとえば、nullの可能性のあるパラメータを扱う2つの方法を比較してください。
public String truncateWordOrNull(String word, int maxLength) {
if (word == null) {
return "";
} else {
return word.substring(0, Math.min(word.length(), maxLength));
}
}
public String truncateWordOrNull(String word, int maxLength) {
try {
return word.substring(0, Math.min(word.length(), maxLength));
} catch (NullPointerException ex) {
return "";
}
}
この例では、 word
がnull
であるかのように、 word
がnull
場合を(意図的に)扱いnull
。 2つのバージョンが扱うnull
場合 、従来の... elseとか...キャッチを試みる使用してどちらか。どんなバージョンが優れているかをどのように判断するべきですか?
最初の基準は読みやすさです。可読性は客観的に数値化することは難しいですが、ほとんどのプログラマは最初のバージョンの本質的な意味が分かりやすいことに同意します。実際に、2番目の形式を真に理解するには、 Math.min
またはString.substring
メソッドでNullPointerException
をスローすることができないことを理解する必要があります。
第2の基準は効率である。 Java 8より前のJavaのリリースでは、2番目のバージョンは最初のバージョンよりも大幅に(桁違いに)遅いです。特に、例外オブジェクトの構築には、スタックトレースが必要な場合に備えて、スタックフレームをキャプチャして記録する必要があります。
一方、例外を使用することは、「例外的な」イベントに対処するために条件付きコードを使用するよりも読みやすく、効率的で、(時には)より正確な多くの状況があります。実際、「例外的でない」イベントにそれらを使用する必要があるまれな状況があります。比較的頻繁に発生する事象である。後者の場合、例外オブジェクトを作成するオーバーヘッドを減らす方法を検討する価値があります。
落とし穴 - 過剰または不適切な積み重ね
プログラマが行うことができるより厄介なことの1つは、コード全体を通してprintStackTrace()
呼び出しを分散させることです。
問題は、 printStackTrace()
がスタックトレースを標準出力に書き込むことです。
Javaプログラマではないエンドユーザを対象としたアプリケーションの場合、スタックトレースは無益であり、最悪の場合には警告です。
サーバー側のアプリケーションでは、誰も標準出力を見ない可能性があります。
より良いアイデアは、 printStackTrace
直接呼び出さないか、それを呼び出す場合は、スタックトレースがエンドユーザのコンソールではなくログファイルまたはエラーファイルに書き込まれるようにします。
これを行う1つの方法は、ロギングフレームワークを使用し、例外オブジェクトをログイベントのパラメータとして渡すことです。しかし、例外をログに記録することさえも、有害な結果が出ると有害になる可能性があります。次の点を考慮してください。
public void method1() throws SomeException {
try {
method2();
// Do something
} catch (SomeException ex) {
Logger.getLogger().warn("Something bad in method1", ex);
throw ex;
}
}
public void method2() throws SomeException {
try {
// Do something else
} catch (SomeException ex) {
Logger.getLogger().warn("Something bad in method2", ex);
throw ex;
}
}
method2
で例外がスローされた場合、同じ失敗に対応するログファイルに同じスタックトレースの2つのコピーが表示される可能性があります。
要するに、例外を記録するか、それをさらにスローします(別の例外でラップされる可能性があります)。両方をしないでください。
落とし穴 - `Throwable`を直接サブクラス化する
Throwable
は、2つの直接的なサブクラスException
とError
ます。 Throwable
直接拡張する新しいクラスを作成することは可能ですが、多くのアプリケーションでException
とError
だけが存在すると想定されるため、これはお勧めできません。
実際には、 Throwable
を直接サブクラス化することは実際にはメリットがありません。結果として得られるクラスは実際にはチェック例外です。代わりにException
をサブクラス化すると、同じ動作になりますが、より明確にあなたの意図を伝えます。