1. 概要

このチュートリアルでは、Javaでの例外処理の基本と、そのいくつかの落とし穴について説明します。

2. 第一原理

2.1. それは何ですか?

例外と例外処理をよりよく理解するために、実際の比較を行いましょう。

オンラインで商品を注文したが、途中で配達に失敗したと想像してみてください。 優れた会社はこの問題を処理し、パッケージが時間どおりに到着するようにパッケージを適切に再ルーティングできます。

同様に、Javaでは、命令の実行中にコードでエラーが発生する可能性があります。 優れた例外処理は、エラーを処理し、プログラムを適切に再ルーティングして、ユーザーに引き続きポジティブなエクスペリエンスを提供できます

2.2. なぜそれを使うのですか? 

通常、理想的な環境でコードを記述します。ファイルシステムには常にファイルが含まれ、ネットワークは正常であり、JVMには常に十分なメモリがあります。 これを「ハッピーパス」と呼ぶこともあります。

ただし、本番環境では、ファイルシステムが破損したり、ネットワークが故障したり、JVMのメモリが不足したりする可能性があります。 私たちのコードの幸福は、それが「不幸な道」をどのように扱うかにかかっています。

これらの条件はアプリケーションのフローに悪影響を及ぼし、例外を形成するため、これらの条件を処理する必要があります。

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. 例外階層

最終的に、例外は単なるJavaオブジェクトであり、それらはすべてThrowableから拡張されています。

              ---> Throwable <--- 
              |    (checked)     |
              |                  |
              |                  |
      ---> Exception           Error
      |    (checked)        (unchecked)
      |
RuntimeException
  (unchecked)

例外的な状態には、主に3つのカテゴリがあります。

  • チェックされた例外
  • 未チェックの例外/ランタイム例外
  • エラー

ランタイムとチェックされていない例外は同じものを参照します。 多くの場合、それらを同じ意味で使用できます。 

3.1. チェックされた例外

チェックされた例外は、Javaコンパイラが処理する必要のある例外です。 宣言的に例外を呼び出しスタックにスローするか、自分で処理する必要があります。 これらの両方については、すぐに詳しく説明します。

Oracleのドキュメントは、メソッドの呼び出し元が回復できると合理的に期待できる場合に、チェックされた例外を使用するように指示しています。

チェックされた例外の例として、IOExceptionServletException。があります。

3.2. 未チェックの例外

チェックされていない例外は、Javaコンパイラが処理する必要のない例外です。

簡単に言うと、 RuntimeException を拡張する例外を作成すると、チェックが外されます。 それ以外の場合はチェックされます。

これは便利に聞こえますが、 Oracleのドキュメントには、状況エラー(チェック済み)と使用エラー(チェックなし)を区別するなど、両方の概念に正当な理由があることが示されています。

未チェックの例外の例としては、 NullPointerException、 IllegalArgumentException、SecurityExceptionがあります。

3.3. エラー

エラーは、ライブラリの非互換性、無限再帰、メモリリークなど、深刻で通常は回復不能な状態を表します。

また、 RuntimeException を拡張していなくても、チェックされていません。

ほとんどの場合、エラーを処理、インスタンス化、または拡張するのは奇妙なことです。 通常、これらを完全に伝播させたいと思います。

エラーの例としては、StackOverflowErrorOutOfMemoryErrorがあります。

4. 例外の処理

Java APIには、問題が発生する可能性のある場所がたくさんあります。これらの場所の一部は、署名またはJavadocのいずれかで例外としてマークされています。

/**
 * @exception FileNotFoundException ...
 */
public Scanner(String fileName) throws FileNotFoundException {
   // ...
}

少し前に述べたように、これらの「危険な」メソッドを呼び出すときは、チェックされた例外を処理する必要があり、チェックされていない例外を処理する可能性があります。 Javaには、これを行うためのいくつかの方法があります。

4.1. スロー

例外を「処理」する最も簡単な方法は、例外を再スローすることです。

public int getPlayerScore(String playerFile)
  throws FileNotFoundException {
 
    Scanner contents = new Scanner(new File(playerFile));
    return Integer.parseInt(contents.nextLine());
}

FileNotFoundException はチェックされた例外であるため、これはコンパイラーを満足させる最も簡単な方法ですが、これは、メソッドを呼び出すすべての人がそれを処理する必要があることを意味します!

parseIntNumberFormatExceptionをスローできますが、チェックされていないため、処理する必要はありません。

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はそれを行う前に最後にの内容を呼び出します。

例外の両方を処理して、リソースが確実に閉じられるようにすることもできます。

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ブロック

場合によっては、コードが複数の例外をスローすることがあり、複数の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 をキャッチしなかったことにも注意してください。これは、が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. ユニオンキャッチブロック

ただし、エラーの処理方法が同じになることがわかっている場合、Java7では同じブロックで複数の例外をキャッチする機能が導入されました。

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. チェックされた例外をスローする

メソッドから戻るのと同じように、いつでもを投げることができます。

もちろん、何かがうまくいかなかったことを示しようとしているときは、投げるべきです。

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を再スローします

今、特別な場合のために。

特定のコードブロックで発生する可能性のある例外がunchecked例外のみである場合、メソッドに追加せずにThrowableまたはExceptionをキャッチして再スローできます。サイン:

public List<Player> loadAllPlayers(String playersFile) {
    try {
        throw new NullPointerException();
    } catch (Throwable t) {
        throw t;
    }
}

単純ですが、上記のコードはチェック例外をスローできません。そのため、チェック例外を再スローしている場合でも、throws句で署名をマークする必要はありません。

これは、プロキシクラスとメソッドで便利です。 これについての詳細は見つけることができますここ

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
    }
}

ただし、「よりより危険な」署名ではありません。

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は catch TimeoutException のみを通知します。これは、 MoreExceptions#loadAllPlayersが別の例外をスローすると言ったので間違っています。

簡単に言えば、サブクラスはスーパークラスよりも少ないチェック済み例外をスローできますが、多いはスローできません。

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);
    }
}

PlayerScoreExceptioncauseとしてIOExceptionを含めることの微妙な違いに注意してください。

6.2. 最後にブロックでreturnを使用する

例外を飲み込むもう1つの方法は、finallyブロックからreturnを実行することです。 これは悪いことです。なぜなら、突然戻ることによって、JVMは、コードによってスローされた場合でも、例外をドロップするからです。

public int getPlayerScore(String playerFile) {
    int score = 0;
    try {
        throw new IOException();
    } finally {
        return score; // <== the IOException is dropped
    }
}

Java言語仕様によると:

他の理由でtryブロックの実行が突然完了した場合R、finallyブロックが実行され、選択肢があります。

finally ブロックが正常に完了すると、理由Rのためにtryステートメントが突然完了します。

finally ブロックが理由Sで突然完了した場合、tryステートメントは理由Sで突然完了します(理由Rは破棄されます)。

6.3. 最後にブロックで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. throwgotoとして使用する

throwgotoステートメントとして使用したいという誘惑に負けた人もいます。

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 – この例外は、StringListに変換しようとしたなど、不正なキャストを実行しようとしたことを意味します。 通常、キャストする前に防御的なインスタンスのチェックを実行することでこれを回避できます。
  • IllegalArgumentException –この例外は、提供されたメソッドまたはコンストラクターパラメーターの1つが無効であると言う一般的な方法です。
  • IllegalStateException –この例外は、オブジェクトの状態などの内部状態が無効であると言う一般的な方法です。
  • NullPointerException –この例外は、nullオブジェクトを参照しようとしたことを意味します。 通常、防御的な null チェックを実行するか、オプションを使用することでこれを回避できます。
  • NumberFormatException –この例外は、 String を数値に変換しようとしたが、「5f3」を数値に変換しようとしたなど、文字列に不正な文字が含まれていたことを意味します。

7.3. エラー

  • StackOverflowError – この例外は、スタックトレースが大きすぎることを意味します。 これは、大規模なアプリケーションで発生することがあります。 ただし、これは通常、コードで無限再帰が発生していることを意味します。
  • NoClassDefFoundError –この例外は、クラスパスにないか、静的初期化の失敗が原因で、クラスのロードに失敗したことを意味します。
  • OutOfMemoryError –この例外は、JVMに、より多くのオブジェクトに割り当てるために使用できるメモリがないことを意味します。 場合によっては、これはメモリリークが原因です。

8. 結論

この記事では、例外処理の基本と、いくつかの良い例と悪い例を説明しました。

いつものように、この記事で見つかったすべてのコードは、GitHubにあります。