例外

要約

  • 例外は異常系の通知メカニズム通常フローには使わないのが原則です。
  • CheckedException系)と UncheckedRuntimeException系)を用途で使い分けます。
  • try-catch-finallytry-with-resourcesマルチキャッチ再スロー例外チェーンを正しく活用します。
  • API設計では意味のあるメッセージ適切な例外型ログと再スローの重複回避が重要です。

1. 例外の基本

  • 例外は発生地点から呼び出し元へ伝播し、どこかで捕捉(catch)されるまで処理が中断されます。
  • スタックトレースは発生箇所の手がかりです。むやみに握りつぶさないでください。
try {
    risky();
} catch (Exception e) {
    e.printStackTrace(); // 実務ではロガーで記録
}

2. Checked と Unchecked の違い

  • CheckedIOException など。宣言 or 捕捉が必須。外部要因(I/O、DB、ネット)由来が中心。
  • UncheckedRuntimeException とそのサブクラス(NullPointerException, IllegalArgumentException 等)。宣言不要呼び出し側のバグや前提違反を表現。
    指針:リトライや回復が期待できる外部要因はChecked、プログラミングエラーはUnchecked

3. try-catch-finally の基本

FileInputStream in = null;
try {
    in = new FileInputStream("data.txt");
    // 読み取り処理
} catch (FileNotFoundException e) {
    // ファイルなしのハンドリング
} catch (IOException e) {
    // 読み書き失敗
} finally {
    if (in != null) try { in.close(); } catch (IOException ignore) {}
}
  • finallyリソース解放の最後の砦です(ただし現代は次項のTWR推奨)。

4. try-with-resources(TWR:AutoCloseable)

import java.io.*;
try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
    String line = br.readLine();
} catch (IOException e) {
    // I/O失敗の対応
}
  • try (...)内のオブジェクトは自動的にcloseされます。
  • 例外が多重発生した場合、サプレスト(抑制)例外getSuppressed() で参照可能。
try (MyRes r = new MyRes()) {
    // 本体で例外
} catch (Exception e) {
    for (Throwable sup : e.getSuppressed()) {
        // 片付け中に起きた副次的な例外
    }
}

5. マルチキャッチ/再スロー/throws

try {
    doIoAndParse();
} catch (IOException | NumberFormatException e) {
    // 似た復旧策をまとめて処理
    recover(e);
    throw e; // さらなる上位へ再スロー(ログ二重化に注意)
}
  • throws呼び出し側へ委ねる設計も有効です。
void load() throws IOException {
    // I/Oは呼び出し側ポリシーで扱ってもらう
}

6. 例外チェーン(causeの連鎖)

try {
    callRemote();
} catch (IOException e) {
    throw new BusinessException("受注登録に失敗しました", e); // 原因を保持
}
  • 元例外をcauseに保持して失跡可能性を確保します(握りつぶし禁止)。

7. カスタム例外の設計

public class BusinessException extends RuntimeException {
    private final String code;
    public BusinessException(String message) { this(message, null, "E-BIZ-0001"); }
    public BusinessException(String message, Throwable cause) { this(message, cause, "E-BIZ-0001"); }
    public BusinessException(String message, Throwable cause, String code) {
        super(message, cause);
        this.code = code;
    }
    public String code() { return code; }
}
  • ドメイン例外RuntimeException継承が一般的。再試行可能性が高いI/O系はException継承も選択肢です。
  • 人間が読めるメッセージ+機械可読なコードの両方を持たせると運用が楽になります。

8. 代表的な標準例外の使い分け

  • IllegalArgumentException:不正な引数
  • IllegalStateException:オブジェクト状態が前提を満たさない
  • NullPointerException:null前提違反(Objects.requireNonNull推奨)
  • UnsupportedOperationException:未サポート機能
  • NoSuchElementException:要素なし
  • IOException:I/O全般
  • TimeoutException / InterruptedException:並行処理の中断・タイムアウト

9. ロギングと再スローの重複回避

  • 原則:ログは「境界」で一度だけ」(例えばWeb/API層)。
  • 下位層では情報を付与して再スローし、最上位で記録するか、下位で記録したら上位では記録しないポリシーを統一します。
  • e.printStackTrace() は開発中限定。実運用はロガー利用(java.util.logging等)。
private static final java.util.logging.Logger LOG =
        java.util.logging.Logger.getLogger(App.class.getName());

try {
    process();
} catch (BusinessException e) {
    LOG.warning(() -> "business error: " + e.getMessage());
    throw e; // ここで記録したなら上位は重複記録しない
}

10. 1ファイル実用サンプル(TWR/マルチキャッチ/チェーン)

import java.io.*;
import java.util.Objects;

public class ExceptionDemo {

    public static void main(String[] args) {
        try {
            int total = loadAndSum("numbers.txt");
            System.out.println("sum = " + total);
        } catch (BusinessException e) {
            // アプリ境界で一度だけログ・ハンドリング
            e.printStackTrace(); // 実運用はロガー
            System.err.println("code=" + e.code() + ", msg=" + e.getMessage());
        }
    }

    // I/O + パース:復旧困難な外部要因(Checked)と入力不正(Unchecked)を分離
    static int loadAndSum(String path) {
        Objects.requireNonNull(path, "path");
        try (BufferedReader br = new BufferedReader(new FileReader(path))) {
            int sum = 0;
            String line;
            int row = 0;
            while ((line = br.readLine()) != null) {
                row++;
                try {
                    sum += Integer.parseInt(line.trim());
                } catch (NumberFormatException e) {
                    // 入力不正:文脈情報を付けてラップ(Unchecked)
                    throw new BusinessException("数値として解釈できません row=" + row + " value=[" + line + "]", e, "E-PARSE-1001");
                }
            }
            return sum;
        } catch (FileNotFoundException e) {
            // 外部要因:ユーザー向けの意味のあるメッセージでラップ
            throw new BusinessException("ファイルが見つかりません: " + path, e, "E-IO-404");
        } catch (IOException e) {
            // I/O一般:原因を保持して上位へ
            throw new BusinessException("ファイルの読み取りに失敗しました: " + path, e, "E-IO-0002");
        }
    }

    // ドメイン例外(Unchecked)
    public static class BusinessException extends RuntimeException {
        private final String code;
        public BusinessException(String msg) { this(msg, null, "E-BIZ-0001"); }
        public BusinessException(String msg, Throwable cause) { this(msg, cause, "E-BIZ-0001"); }
        public BusinessException(String msg, Throwable cause, String code) { super(msg, cause); this.code = code; }
        public String code() { return code; }
    }
}
  • numbers.txt の各行に整数が並ぶ想定です。
  • 行の一部が不正でもどの行が原因かを例外メッセージで把握できます。

ベストプラクティス要点

  • 例外は異常系のみに使用。通常の分岐は制御構文で表現します。
  • Checked=外部要因Unchecked=前提違反の住み分けを徹底します。
  • TWRで確実にクローズ。多重例外はgetSuppressed()で追跡します。
  • 意味のあるメッセージ+原因(cause)保持。握りつぶさず上位の境界で一度だけログ
  • APIはthrows方針を明文化し、呼び出し側が対処しやすい設計にします。