Javaでの例外処理
1. 概要
このチュートリアルでは、Javaでの例外処理の基本と、そのいくつかの落とし穴について説明します。
2. 第一原理
2.1. それは何ですか?
例外と例外処理をよりよく理解するために、実際の比較を行いましょう。
オンラインで商品を注文したが、途中で配達に失敗したと想像してみてください。 良い会社はこの問題に対処し、パッケージが適切な時間に届くように適切に再ルーティングできます。
同様に、Javaでは、命令の実行中にコードでエラーが発生する可能性があります。 優れたexception handlingはエラーを処理し、プログラムを適切に再ルーティングして、ユーザーにポジティブなエクスペリエンスを提供します.
2.2. なぜそれを使うの?
通常、コードは理想的な環境で作成します。ファイルシステムには常にファイルが含まれ、ネットワークは正常で、JVMには常に十分なメモリがあります。 これを「ハッピーパス」と呼ぶこともあります。
ただし、実稼働環境では、ファイルシステムが破損し、ネットワークが故障し、JVMのメモリが不足する可能性があります。 私たちのコードの幸福は、それが「不幸な道」をどのように扱うかに依存します。
これらの条件はアプリケーションのフローに悪影響を及ぼし、exceptionsを形成するため、これらの条件を処理する必要があります。
public static List getPlayers() throws IOException {
Path path = Paths.get("players.dat");
List players = Files.readAllLines(path);
return players.stream()
.map(Player::new)
.collect(Collectors.toList());
}
このコードは、IOExceptionを処理せず、代わりにコールスタックに渡すことを選択します。 理想的な環境では、コードは正常に機能します。
しかし、players.dat isが欠落している場合、本番環境で何が起こる可能性がありますか?
Exception in thread "main" java.nio.file.NoSuchFileException: players.dat <-- players.dat file doesn't exist
at sun.nio.fs.WindowsException.translateToIOException(Unknown Source)
at sun.nio.fs.WindowsException.rethrowAsIOException(Unknown Source)
// ... more stack trace
at java.nio.file.Files.readAllLines(Unknown Source)
at java.nio.file.Files.readAllLines(Unknown Source)
at Exceptions.getPlayers(Exceptions.java:12) <-- Exception arises in getPlayers() method, on line 12
at Exceptions.main(Exceptions.java:19) <-- getPlayers() is called by main(), on line 19
Without handling this exception, an otherwise healthy program may stop running altogether!コードに問題が発生した場合の計画があることを確認する必要があります。
また、例外に対するもう1つの利点もあります。これはスタックトレース自体です。 このスタックトレースにより、多くの場合、デバッガをアタッチせずに問題のコードを特定できます。
3. 例外階層
最終的に、exceptions は単なるJavaオブジェクトであり、それらはすべてThrowableから拡張されます。
---> Throwable <---
| (checked) |
| |
| |
---> Exception Error
| (checked) (unchecked)
|
RuntimeException
(unchecked)
例外条件には、主に3つのカテゴリがあります。
-
チェック済みの例外
-
未チェックの例外/ランタイム例外
-
エラー
ランタイム例外と未チェックの例外は同じものを参照します。 多くの場合、それらは同じ意味で使用できます。
3.1. Checked Exceptions
チェック例外は、Javaコンパイラが処理する必要がある例外です。 例外を呼び出しスタックに宣言的にスローするか、自分で処理する必要があります。 これらの両方についてはすぐに説明します。
Oracle’s documentationは、メソッドの呼び出し元が回復できると合理的に期待できる場合に、チェックされた例外を使用するように指示します。
チェックされた例外の例としては、IOExceptionとServletException.があります。
3.2. 未確認の例外
チェックされていない例外は、Javaコンパイラがnotで処理する必要のある例外です。
簡単に言うと、RuntimeExceptionを拡張する例外を作成すると、チェックが外されます。それ以外の場合はチェックされます。
これは便利に聞こえますが、Oracle’s documentationは、状況エラー(チェック済み)と使用エラー(チェックなし)を区別するなど、両方の概念に正当な理由があることを示しています。
未チェックの例外の例としては、NullPointerException, IllegalArgumentException, とSecurityExceptionがあります。
3.3. エラー
エラーは重大であり、通常はライブラリの非互換性、無限再帰、メモリリークなどの回復不能な状態を表します。
また、RuntimeExceptionを拡張していなくても、チェックされていません。
ほとんどの場合、Errorsを処理、インスタンス化、または拡張するのはおかしいでしょう。 通常、これらを完全に伝播させたいです。
エラーの例としては、StackOverflowErrorとOutOfMemoryErrorがあります。
4. 例外処理
Java APIには、問題が発生する可能性のある場所がたくさんあり、これらの場所のいくつかは、署名またはJavadocのいずれかで例外のマークが付けられています。
/**
* @exception FileNotFoundException ...
*/
public Scanner(String fileName) throws FileNotFoundException {
// ...
}
少し前に述べたように、これらの「危険な」メソッドを呼び出すと、mustはチェックされた例外を処理し、mayはチェックされていない例外を処理します。 Javaには、これを行うためのいくつかの方法があります。
4.1. throws
例外を「処理」する最も簡単な方法は、例外を再スローすることです。
public int getPlayerScore(String playerFile)
throws FileNotFoundException {
Scanner contents = new Scanner(new File(playerFile));
return Integer.parseInt(contents.nextLine());
}
FileNotFoundException はチェックされた例外であるため、これはコンパイラを満足させる最も簡単な方法ですが、it does mean that anyone that calls our method now needs to handle it too!
parseIntはNumberFormatExceptionをスローできますが、チェックされていないため、処理する必要はありません。
4.2. try –catch
自分で例外を処理したい場合は、try-catchブロックを使用できます。 例外を再スローすることで処理できます。
public int getPlayerScore(String playerFile) {
try {
Scanner contents = new Scanner(new File(playerFile));
return Integer.parseInt(contents.nextLine());
} catch (FileNotFoundException noFile) {
throw new IllegalArgumentException("File not found");
}
}
または、リカバリ手順を実行することにより:
public int getPlayerScore(String playerFile) {
try {
Scanner contents = new Scanner(new File(playerFile));
return Integer.parseInt(contents.nextLine());
} catch ( FileNotFoundException noFile ) {
logger.warn("File not found, resetting score.");
return 0;
}
}
4.3. finally
現在、例外が発生したかどうかに関係なく実行する必要のあるコードがある場合があります。ここで、finallyキーワードが使用されます。
これまでの例では、影に潜んでいる厄介なバグがありました。つまり、Javaはデフォルトでファイルハンドルをオペレーティングシステムに返しません。
確かに、ファイルを読み取ることができるかどうかにかかわらず、適切なクリーンアップを確実に行いたいと思います!
最初にこれを「怠惰な」方法で試してみましょう。
public int getPlayerScore(String playerFile)
throws FileNotFoundException {
Scanner contents = null;
try {
contents = new Scanner(new File(playerFile));
return Integer.parseInt(contents.nextLine());
} finally {
if (contents != null) {
contents.close();
}
}
}
ここで、finallyブロックは、ファイルを読み取ろうとして何が起こったかに関係なく、Javaで実行するコードを示します。
FileNotFoundExceptionがコールスタックにスローされた場合でも、Javaはそれを行う前にfinallyのコンテンツを呼び出します。
両方とも例外andを処理して、リソースが確実に閉じられるようにすることもできます。
public int getPlayerScore(String playerFile) {
Scanner contents;
try {
contents = new Scanner(new File(playerFile));
return Integer.parseInt(contents.nextLine());
} catch (FileNotFoundException noFile ) {
logger.warn("File not found, resetting score.");
return 0;
} finally {
try {
if (contents != null) {
contents.close();
}
} catch (IOException io) {
logger.error("Couldn't close the reader!", io);
}
}
}
closeも「危険な」メソッドであるため、その例外をキャッチする必要もあります。
これはかなり複雑に見えるかもしれませんが、正しく発生する可能性のある各問題を処理するために各要素が必要です。
4.4. try-with-resources
幸い、Java 7の時点で、AutoCloseableを拡張するものを操作するときに、上記の構文を簡略化できます。
public int getPlayerScore(String playerFile) {
try (Scanner contents = new Scanner(new File(playerFile))) {
return Integer.parseInt(contents.nextLine());
} catch (FileNotFoundException e ) {
logger.warn("File not found, resetting score.");
return 0;
}
}
try 宣言にAutoClosable である参照を配置する場合、リソースを自分で閉じる必要はありません。
ただし、finallyブロックを使用して、他の種類のクリーンアップを実行することはできます。
詳細については、try-with-resources専用の記事をご覧ください。
4.5. 複数のcatchブロック
コードが複数の例外をスローする場合があり、複数のcatchブロックでそれぞれを個別に処理することができます。
public int getPlayerScore(String playerFile) {
try (Scanner contents = new Scanner(new File(playerFile))) {
return Integer.parseInt(contents.nextLine());
} catch (IOException e) {
logger.warn("Player file wouldn't load!", e);
return 0;
} catch (NumberFormatException e) {
logger.warn("Player file was corrupted!", e);
return 0;
}
}
複数のキャッチにより、必要に応じて各例外を異なる方法で処理する機会が与えられます。
また、FileNotFoundExceptionをキャッチしなかったことにも注意してください。これは、extends IOExceptionであるためです。 IOExceptionをキャッチしているため、Javaはそのサブクラスも処理されていると見なします。
ただし、FileNotFoundException をより一般的なIOExceptionとは異なる方法で処理する必要があるとします。
public int getPlayerScore(String playerFile) {
try (Scanner contents = new Scanner(new File(playerFile)) ) {
return Integer.parseInt(contents.nextLine());
} catch (FileNotFoundException e) {
logger.warn("Player file not found!", e);
return 0;
} catch (IOException e) {
logger.warn("Player file wouldn't load!", e);
return 0;
} catch (NumberFormatException e) {
logger.warn("Player file was corrupted!", e);
return 0;
}
}
Javaでは、サブクラスの例外を個別に処理できます、remember to place them higher in the list of catches.
4.6. ユニオンcatchブロック
ただし、エラーの処理方法が同じになることがわかっている場合、Java 7では同じブロックで複数の例外をキャッチする機能が導入されました。
public int getPlayerScore(String playerFile) {
try (Scanner contents = new Scanner(new File(playerFile))) {
return Integer.parseInt(contents.nextLine());
} catch (IOException | NumberFormatException e) {
logger.warn("Failed to load score!", e);
return 0;
}
}
5. 例外を投げる
自分で例外を処理したくない場合、または他の人が処理できるように例外を生成したい場合は、throwキーワードに精通する必要があります。
自分で作成した次のチェック済み例外があるとします。
public class TimeoutException extends Exception {
public TimeoutException(String message) {
super(message);
}
}
完了するまでに時間がかかる可能性のあるメソッドがあります。
public List loadAllPlayers(String playersFile) {
// ... potentially long operation
}
5.1. チェックされた例外をスローする
メソッドから戻るのと同じように、任意の時点でthrow を実行できます。
もちろん、何かが間違っていることを示しようとしているときは投げるべきです:
public List loadAllPlayers(String playersFile) throws TimeoutException {
while ( !tooLong ) {
// ... potentially long operation
}
throw new TimeoutException("This operation took too long");
}
TimeoutExceptionがチェックされているため、署名でthrowsキーワードを使用して、メソッドの呼び出し元がそれを処理できるようにする必要があります。
5.2. Throwチェックされていない例外を歌う
入力の検証などの処理を行いたい場合は、代わりに未チェックの例外を使用できます。
public List loadAllPlayers(String playersFile) throws TimeoutException {
if(!isFilenameValid(playersFile)) {
throw new IllegalArgumentException("Filename isn't valid!");
}
// ...
}
IllegalArgumentExceptionがオフになっているため、メソッドにマークを付ける必要はありませんが、歓迎します。
とにかくメソッドをドキュメントの形式としてマークする人もいます。
5.3. ラッピングと再スロー
キャッチした例外を再スローすることもできます。
public List loadAllPlayers(String playersFile)
throws IOException {
try {
// ...
} catch (IOException io) {
throw io;
}
}
または、ラップして再スローします。
public List loadAllPlayers(String playersFile)
throws PlayerLoadException {
try {
// ...
} catch (IOException io) {
throw new PlayerLoadException(io);
}
}
これは、多くの異なる例外を1つに統合するのに便利です。
5.4. ThrowableまたはExceptionの再スロー
特別な場合に。
特定のコードブロックで発生する可能性のある例外がunchecked例外のみである場合、メソッドシグネチャに追加せずにThrowableまたはException をキャッチして再スローできます。
public List loadAllPlayers(String playersFile) {
try {
throw new NullPointerException();
} catch (Throwable t) {
throw t;
}
}
単純ですが、上記のコードはチェックされた例外をスローできません。そのため、チェックされた例外を再スローしている場合でも、署名にthrows 句を付ける必要はありません。
This is handy with proxy classes and methods.これについての詳細はhereにあります。
5.5. Inheritance
メソッドをthrowsキーワードでマークすると、サブクラスがメソッドをオーバーライドする方法に影響します。
メソッドがチェック済み例外をスローする状況では:
public class Exceptions {
public List loadAllPlayers(String playersFile)
throws TimeoutException {
// ...
}
}
サブクラスには、「リスクの少ない」署名を付けることができます。
public class FewerExceptions extends Exceptions {
@Override
public List loadAllPlayers(String playersFile) {
// overridden
}
}
ただし、「more riskier」署名ではありません。
public class MoreExceptions extends Exceptions {
@Override
public List loadAllPlayers(String playersFile) throws MyCheckedException {
// overridden
}
}
これは、コントラクトが参照型によってコンパイル時に決定されるためです。 MoreExceptions のインスタンスを作成し、それをExceptionsに保存した場合:
Exceptions exceptions = new MoreExceptions();
exceptions.loadAllPlayers("file");
次に、JVMはcatchにTimeoutExceptionのみを通知しますが、MoreExceptions#loadAllPlayersは別の例外をスローすると言ったので、これは間違っています。
簡単に言うと、サブクラスはスーパークラスよりもfewerのチェック済み例外をスローできますが、moreはスローできません。
6. アンチパターン
6.1. 嚥下の例外
さて、コンパイラーを満足させる方法がもう1つあります。
public int getPlayerScore(String playerFile) {
try {
// ...
} catch (Exception e) {} // <== catch and swallow
return 0;
}
The above is calledswallowing an exception。 ほとんどの場合、これを行うことは、問題andに対処しないため、他のコードでも問題に対処できないため、少し意味があります。
チェックされた例外があり、決して発生しないと確信している場合があります。 In those cases, we should still at least add a comment stating that we intentionally ate the exception:
public int getPlayerScore(String playerFile) {
try {
// ...
} catch (IOException e) {
// this will never happen
}
}
例外を「飲み込む」ことができる別の方法は、単純に例外をエラーストリームに出力することです。
public int getPlayerScore(String playerFile) {
try {
// ...
} catch (Exception e) {
e.printStackTrace();
}
return 0;
}
後で診断できるようにエラーをどこかに書き出すことで、状況を少し改善しました。
ただし、ロガーを使用する方がよいでしょう。
public int getPlayerScore(String playerFile) {
try {
// ...
} catch (IOException e) {
logger.error("Couldn't load the score", e);
return 0;
}
}
この方法で例外を処理することは非常に便利ですが、コードの呼び出し元が問題を解決するために使用できる重要な情報を飲み込まないようにする必要があります。
最後に、新しい例外をスローするときに原因として例外を含めないことで、誤って例外を飲み込むことができます。
public int getPlayerScore(String playerFile) {
try {
// ...
} catch (IOException e) {
throw new PlayerScoreException();
}
}
ここでは、発信者にエラーを警告するために背中を軽くたたきますが、we fail to include the IOException as the cause.このため、発信者またはオペレーターが問題を診断するために使用できる重要な情報が失われました。
次のことを行うほうがよいでしょう。
public int getPlayerScore(String playerFile) {
try {
// ...
} catch (IOException e) {
throw new PlayerScoreException(e);
}
}
PlayerScoreExceptionのcauseとしてIOExceptionを含めることの微妙な違いに注意してください。
6.2. finallyブロックでreturnを使用する
例外を飲み込む別の方法は、finallyブロックからreturnを実行することです。 これは、コードからスローされた場合でも、JVMが突然戻ることで例外をドロップするため、悪いことです。
public int getPlayerScore(String playerFile) {
int score = 0;
try {
throw new IOException();
} finally {
return score; // <== the IOException is dropped
}
}
Rの他の理由でtryブロックの実行が突然完了した場合、finallyブロックが実行され、選択肢があります。
finallyブロックが正常に完了すると、理由Rのためにtryステートメントが突然完了します。
finallyブロックが理由Sで突然完了した場合、tryステートメントは理由Sで突然完了します(理由Rは破棄されます)。
6.3. finallyブロックでthrowを使用する
finallyブロックでreturnを使用するのと同様に、finallyブロックでスローされた例外は、catchブロックで発生した例外よりも優先されます。
これにより、tryブロックから元の例外が「消去」され、その貴重な情報がすべて失われます。
public int getPlayerScore(String playerFile) {
try {
// ...
} catch ( IOException io ) {
throw new IllegalStateException(io); // <== eaten by the finally
} finally {
throw new OtherException();
}
}
6.4. throwをgotoとして使用する
一部の人々はまた、throwをgotoステートメントとして使用する誘惑に屈しました。
public void doSomething() {
try {
// bunch of code
throw new MyException();
// second bunch of code
} catch (MyException e) {
// third bunch of code
}
}
エラー処理ではなく、コードがフロー制御に例外を使用しようとしているため、これは奇妙です。
7. 一般的な例外とエラー
よくある例外とエラーは次のとおりです。
7.1. チェック済みの例外
-
IOException –この例外は通常、ネットワーク、ファイルシステム、またはデータベース上の何かが失敗したことを示す方法です。
7.2. RuntimeException
-
ArrayIndexOutOfBoundsException –この例外は、長さ3の配列からインデックス5を取得しようとしたときのように、存在しない配列インデックスにアクセスしようとしたことを意味します。
-
ClassCastException –この例外は、StringをListに変換しようとするなど、不正なキャストを実行しようとしたことを意味します。 通常、キャストする前に防御的なinstanceof checkを実行することでこれを回避できます。
-
IllegalArgumentException –この例外は、提供されたメソッドまたはコンストラクターパラメーターの1つが無効であると言う一般的な方法です。
-
IllegalStateException –この例外は、オブジェクトの状態と同様に、内部状態が無効であると言う一般的な方法です。
-
NullPointerException –この例外は、nullオブジェクトを参照しようとしたことを意味します。 通常、防御的なnullチェックを実行するか、Optional.を使用することで、これを回避できます。
-
NumberFormatException –この例外は、Stringを数値に変換しようとしたが、「5f3」を数値に変換しようとしたなど、文字列に不正な文字が含まれていたことを意味します。
7.3. エラー
-
StackOverflowError –この例外は、スタックトレースが大きすぎることを意味します。 これは、大規模なアプリケーションで発生することがあります。ただし、通常は、コード内で無限の再帰が発生していることを意味します。
-
NoClassDefFoundError –この例外は、クラスパスにないか、静的初期化の失敗が原因で、クラスのロードに失敗したことを意味します。
-
OutOfMemoryError –この例外は、JVMに、より多くのオブジェクトに割り当てるために使用できるメモリがないことを意味します。 時々、これはメモリリークが原因です。
8. 結論
この記事では、例外処理の基本と、いくつかの良い練習例と悪い練習例について説明しました。
いつものように、この記事で見つかったすべてのコードはover on GitHubで見つかります!