Javaでの例外処理
1概要
このチュートリアルでは、Javaでの例外処理の基本とそのいくつかの問題点について説明します。
2第一原則
2.1. それは何ですか?
例外と例外処理をよりよく理解するために、実際の比較をしましょう。
オンラインで商品を注文したが、途中で配送に失敗したとします。良い会社がこの問題を処理して、パッケージが時間どおりに届くようにパッケージを適切に再ルーティングできます。
同様に、Javaでは、コードは私達の命令を実行している間エラーを経験することができます。良い例外処理
はエラーを処理し、プログラムを適切に再ルーティングしてユーザーにまだ良い経験を与えることができます
.
__
2.2. なぜそれを使うの?
私たちは通常、理想的な環境でコードを書きます。ファイルシステムは常に私たちのファイルを含み、ネットワークは健全で、JVMは常に十分なメモリを持っています。私たちはこれを「幸せな道」と呼ぶことがあります。
しかし本番環境では、ファイルシステムが破損し、ネットワークが故障し、JVMのメモリが不足する可能性があります。私たちのコードの健康は、それが「不幸な道」をどのように扱うかにかかっています。
これらの条件はアプリケーションのフローに悪影響を及ぼし、
exceptions
を形成するため、これらの条件を処理する必要があります。
public static List<Player> getPlayers() throws IOException {
Path path = Paths.get("players.dat");
List<String> players = Files.readAllLines(path);
return players.stream()
.map(Player::new)
.collect(Collectors.toList());
}
このコードは
IOException
を処理しないことを選択し、代わりにそれを呼び出しスタックに渡します。理想的な環境では、コードは正常に機能します。
しかし、
__players.dat
__が見つからない場合、プロダクションで何が起こるのでしょうか?
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
-
この例外を処理しないと、それ以外の点では問題ないプログラムが完全に実行を停止する可能性があります。
また、例外に対するもう1つの利点があります。これはスタックトレース自体です。このスタックトレースにより、デバッガをアタッチする必要なしに、問題のあるコードを特定することができます。
3例外階層
結局のところ、
__exceptionは単なるJavaオブジェクトであり、それらはすべて
Throwable__から拡張されています。
---> Throwable <---
| (checked) |
| |
| |
---> Exception Error
| (checked) (unchecked)
|
RuntimeException
(unchecked)
例外条件には、主に3つのカテゴリがあります。
-
チェック済みの例外
-
未チェック例外/ランタイム例外
-
エラー
-
実行時例外と未チェック例外は同じものを参照します。私たちはしばしばそれらを互換的に使うことができます。 **
3.1.
チェック済み例外
チェック例外は、Javaコンパイラが処理を要求する例外です。宣言を使用してコールスタックに例外をスローするか、自分で処理する必要があります。これらの両方について、もう少し詳しく説明します。
Oracleのドキュメント
は、このメソッドの呼び出し元が回復することを合理的に期待できる場合にチェック例外を使用するように指示しています。
チェック例外の例としては、
IOException
と__ServletExceptionがあります。
3.2. 未チェックの例外
未チェックの例外は、Javaコンパイラが処理を要求しないという例外です。
簡単に言えば、
RuntimeException
を拡張する例外を作成した場合、それはチェックされません。それ以外の場合はチェックされます。
そしてこれは便利そうに思えますが、https://docs.oracle.com/javase/tutorial/essential/exceptions/runtime.html[Oracleのドキュメント]は、状況エラーを区別するような両方の概念には正当な理由があることを示しています)および使用方法エラー(未チェック)
未チェックの例外の例としては、
__ NullPointerException、IllegalArgumentException、
および
SecurityException__があります。
3.3. エラー
エラーは、ライブラリの非互換性、無限の再帰、メモリリークなど、深刻で通常は回復不可能な状態を表します。
たとえ
RuntimeException
を継承していなくても、チェックは外されています。
ほとんどの場合、
Errors
を処理、インスタンス化、または拡張するのは奇妙なことです。通常、私たちはこれらがずっと上に伝播することを望みます。
エラーの例としては、
StackOverflowError
と
OutOfMemoryError
があります。
4例外処理
Java APIには、問題が発生する可能性がある場所がたくさんあります。これらの場所のいくつかは、シグネチャまたはJavadocのいずれかで例外としてマークされています。
----/** **
** @exception FileNotFoundException ...
** /public Scanner(String fileName) throws FileNotFoundException {
//...
}
----
少し前に述べたように、これらの “危険な”メソッドを呼ぶとき、チェックされた例外を扱う必要があり、チェックされていない例外を扱う必要があります。 Javaはこれを行うためのいくつかの方法を提供しています。
4.1.
throws
例外を「処理」する最も簡単な方法は、例外を再スローすることです。
public int getPlayerScore(String playerFile)
throws FileNotFoundException {
Scanner contents = new Scanner(new File(playerFile));
return Integer.parseInt(contents.nextLine());
}
____FileNotFoundExceptionはチェックされた例外なので、これはコンパイラを満足させる最も簡単な方法ですが、** これは、私たちのメソッドを呼び出すすべての人がそれを処理する必要があることを意味します。
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
キーワードが来るところです。
これまでの例では、影に潜んでいる厄介なバグがあります。それは、デフォルトでは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
の内容を呼び出します。
また、両方とも例外を処理して、リソースを確実に閉じます。
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;
}
}
__AutoClosableである参照を
__try宣言で配置する場合、リソースを自分で閉じる必要はありません。
ただし、他の種類のクリーンアップを実行するために
finally
ブロックを使用することもできます。
詳しくは
try
-with-resources
をご覧ください。
4.5. 複数のキャッチブロック
ときには、コードが複数の例外をスローすることがあり、それぞれが個別に複数の
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
をキャッチしていないことに注意してください。これは、
Exceptionが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では、サブクラスの例外を個別に処理できます。** キャッチリストの上位に置くことを忘れないでください。
4.6. ユニオンキャッチブロック
ただし、エラー処理の方法が同じになることがわかっている場合は、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<Player> loadAllPlayers(String playersFile) {
//... potentially long operation
}
5.1. チェックされた例外を投げる
メソッドから戻るのと同じように、いつでも
__throw
__することができます。
もちろん、何か問題が発生したことを示すときにはスローする必要があります。
public List<Player> loadAllPlayers(String playersFile) throws TimeoutException {
while ( !tooLong ) {
//... potentially long operation
}
throw new TimeoutException("This operation took too long");
}
TimeoutException
がチェックされているので、私たちのメソッドの呼び出し側がそれを処理することを知っているように、私たちはまたシグネチャで
throws
キーワードを使わなければなりません。
5.2. 未チェックの例外をスローする
例えば入力を検証するなどのことをしたい場合は、代わりに未チェックの例外を使用できます。
public List<Player> loadAllPlayers(String playersFile) throws TimeoutException {
if(!isFilenameValid(playersFile)) {
throw new IllegalArgumentException("Filename isn't valid!");
}
//...
}
IllegalArgumentException
はチェックされていないので、歓迎しますが、メソッドにマークを付ける必要はありません。
とにかくメソッドをドキュメントの形式としてマークする人もいます。
5.3. ラッピングと再投げ
キャッチした例外を再スローすることもできます。
public List<Player> loadAllPlayers(String playersFile)
throws IOException {
try {
//...
} catch (IOException io) {
throw io;
}
}
またはラップして再スローします。
public List<Player> loadAllPlayers(String playersFile)
throws PlayerLoadException {
try {
//...
} catch (IOException io) {
throw new PlayerLoadException(io);
}
}
これは、さまざまな例外を1つにまとめるのに便利です。
5.4.
Throwable
または
Exception
を再スローしています
今度は特別な場合です。
特定のコードブロックで発生する可能性のある唯一の例外が
未チェックの例外である場合、それらをメソッドのシグネチャに追加せずに、
Throwable
または
__Exceptionをキャッチして再スローできます。
public List<Player> loadAllPlayers(String playersFile) {
try {
throw new NullPointerException();
} catch (Throwable t) {
throw t;
}
}
単純ですが、上記のコードではチェック済み例外をスローできません。そのため、チェック済み例外を再スローしても、署名に____throw句を付ける必要はありません。
-
これはプロキシのクラスやメソッドと一緒に使うのが便利です。
5.5.
継承
throws
キーワードでメソッドをマークすると、サブクラスがどのように私たちのメソッドをオーバーライドできるかに影響します。
私たちのメソッドがチェックされた例外をスローする状況では
public class Exceptions {
public List<Player> loadAllPlayers(String playersFile)
throws TimeoutException {
//...
}
}
サブクラスは「リスクが少ない」というシグネチャを持つことができます。
public class FewerExceptions extends Exceptions {
@Override
public List<Player> loadAllPlayers(String playersFile) {
//overridden
}
}
しかし、“
__ more
__riskier”の署名ではありません。
public class MoreExceptions extends Exceptions {
@Override
public List<Player> loadAllPlayers(String playersFile) throws MyCheckedException {
//overridden
}
}
これは、コントラクトが参照時の型によってコンパイル時に決定されるためです。
__MoreExceptions
のインスタンスを作成し、それを
Exceptions__に保存したとします。
Exceptions exceptions = new MoreExceptions();
exceptions.loadAllPlayers("file");
その場合、JVMは
TimeoutException
をキャッチするように指示するだけですが、これは
MoreExceptions#loadAllPlayers
が別の例外をスローすると言ったので間違っています。
簡単に言うと、サブクラスはスーパークラスより
fewer
チェック例外をスローできますが、
more
はスローできません。
6. アンチパターン
6.1. 飲み込む例外
さて、もう1つ、コンパイラを満足させる方法があります。
public int getPlayerScore(String playerFile) {
try {
//...
} catch (Exception e) {}//<== catch and swallow
return 0;
}
-
上記は
例外を飲み込む** と呼ばれます。ほとんどの場合、これを実行するのは少し意味があります。問題を解決できず、他のコードが問題を解決できないからです。
私たちが自信を持っていると確信しているという確認済みの例外が発生することは決してないでしょう。 ** そのような場合は、少なくとも意図的に例外を食べたことを示すコメントを少なくとも追加する必要があります。
public int getPlayerScore(String playerFile) {
try {
//...
} catch (IOException e) {
//this will never happen
}
}
例外を「飲み込む」ことができるもう1つの方法は、単に例外をエラーストリームに出力することです。
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();
}
}
ここでは、呼び出し側にエラーを警告するために腰を下ろしましたが、
IOException
を原因として含めることはできません
このため、呼び出し側またはオペレータが問題の診断に使用できる重要な情報を失いました。
次のようにしたほうがよいでしょう。
public int getPlayerScore(String playerFile) {
try {
//...
} catch (IOException e) {
throw new PlayerScoreException(e);
}
}
PlayerScoreException
の
cause
として
IOException
を含めることの微妙な違いに注意してください。
6.2.
finally
ブロックで
return
を使用する
例外を飲み込むもう1つの方法は、finalブロックから
return
することです。これは悪いことです。なぜなら、突然戻ってきたことで、JVMが例外をドロップしてしまうからです。
public int getPlayerScore(String playerFile) {
int score = 0;
try {
throw new IOException();
} finally {
return score;//<== the IOException is dropped
}
}
Java言語仕様
によると、
他の何らかの理由でtryブロックの実行が突然完了した場合は、finallyブロックが実行され、その後選択がある。
finally
ブロックが正常に完了した場合、tryステートメントは理由Rで突然完了します。
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.
goto
として
throw
を使用する
goto
文として
throw
を使用したいという誘惑にも惹かれる人がいます。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. RuntimeExceptions
ArrayIndexOutOfBoundsException
– この例外は、試したことを意味します長さ3の配列からインデックス5を取得するときのように、存在しない配列インデックスにアクセスする。
__ClassCastException – この例外は、実行しようとしたことを意味します
String
を
List
に変換しようとするなど、不正なキャストです。通常、キャストの前に防御的な
__instanceof
__checksを実行することでこれを回避できます。
IllegalArgumentException
– この例外は、一般的な方法です。提供されたメソッドまたはコンストラクタのパラメータの1つが無効であると言います。
IllegalStateException
– この例外は、一般的な方法です。オブジェクトの状態のように、内部の状態は無効であると言います。
NullPointerException
– この例外は、aを参照しようとしたことを意味します。
null
オブジェクト。私たちは防御のどちらかを実行することによって通常それを避けることができます
null
チェックまたは
Optional.
を使用して
**
NumberFormatException
– この例外は、試みたことを意味します
String
を数値に変換しましたが、文字列に「5f3」を数値に変換しようとしたときのように不正な文字が含まれていました。====
7.3. エラー
__StackOverflowError – この例外は、スタックトレースが
大きすぎる。これは時々大規模なアプリケーションで発生する可能性があります。しかし、それは通常、コード内で無限の再帰が発生していることを意味します。
NoClassDefFoundError
– この例外はクラスが失敗したことを意味しますクラスパス上にないか、静的初期化の失敗によるものです。
OutOfMemoryError
– この例外はJVMが持っていないことを意味しますより多くのオブジェクトに割り当てるために使用可能なメモリがあればそれ以上。時々、これはメモリリークが原因です。
===
8結論
この記事では、例外処理の基本と、良い例と悪い例をいくつか紹介しました。
いつものように、この記事で見つけたすべてのコードはhttps://github.com/eugenp/tutorials/tree/master/core-java-lang[over on GitHub]にあります。