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]にあります。