Java | 基礎文法:例外とは

Java Java
スポンサーリンク

例外の全体像

例外(Exception)は「通常の処理の流れを中断して、異常事態を安全に伝える仕組み」です。ファイルがない、ネットワークが切れた、引数が不正、ゼロ除算など「このまま続けると壊れる」状況で、例外を投げて(throw)呼び出し側へ「失敗の事実」と「原因」を届けます。例外を正しく設計・処理できると、プログラムは「失敗しても安全に着地」できるようになります。


仕組みとクラス階層

例外はオブジェクト(Throwable の系譜)

例外はオブジェクトで、Throwable を頂点とするクラス階層に属します。代表は Exception(一般的な例外)と Error(JVMレベルの致命的エラー)。Error はアプリ側で回復しない前提なので、通常は捕捉しません。

Throwable
├─ Error              (OutOfMemoryError など/原則扱わない)
└─ Exception
   ├─ RuntimeException(実行時例外/チェック不要)
   └─ チェック例外     (IOException, SQLException など)

例外は「投げる(throw)」「捕まえる(catch)」「伝播させる(throws)」の3動作で扱います。


チェック例外と実行時例外(重要ポイントの深掘り)

2種類の違いと設計指針

  • チェック例外(IOException など):コンパイラが「処理せよ」と要求。呼び出し側が現実的に回復できる状況向け(再試行、別パス選択など)。
  • 実行時例外(RuntimeExceptionNullPointerExceptionIllegalArgumentException など):プログラムのバグや契約違反。呼び出し側での即時回復は想定せず、入力検証・修正で防ぐべき。

設計の要点は「呼び出し側が合理的に回復できるならチェック例外、そうでなければ実行時例外」。曖昧なら、まずは原因(仕様違反か外部要因か)を言語化してから選びます。


使い方の基本:try-catch-finally と throws

例外を捕まえる(try-catch)

try {
    var br = new java.io.BufferedReader(new java.io.FileReader("input.txt"));
    System.out.println(br.readLine());
    br.close();
} catch (java.io.FileNotFoundException e) {
    System.err.println("ファイルが見つかりません: " + e.getMessage());
} catch (java.io.IOException e) {
    System.err.println("読み込み失敗: " + e.getMessage());
}
Java

合目的な単位を try で囲み、失敗の種類に応じた catch を並べます。メッセージは「何が・なぜ」を短く具体的に。

後処理は finally(または try-with-resources)

java.io.BufferedReader br = null;
try {
    br = new java.io.BufferedReader(new java.io.FileReader("input.txt"));
    System.out.println(br.readLine());
} catch (java.io.IOException e) {
    System.err.println("I/O失敗: " + e.getMessage());
} finally {
    if (br != null) try { br.close(); } catch (java.io.IOException ignored) {}
}
Java

finally は「例外の有無に関わらず必ず実行」される後始末用。I/Oは後述の try-with-resources が推奨です。

例外を呼び出し側へ伝える(throws)

void load(java.nio.file.Path p) throws java.io.IOException {
    try (var br = java.nio.file.Files.newBufferedReader(p)) {
        System.out.println(br.readLine());
    } // ここで自動 close
}
Java

「ここで回復しない」判断なら、throws で伝播し、上位でまとめて扱います。無闇な「握り潰し」は禁物です。


例外メッセージと原因連鎖(重要ポイントの深掘り)

原因例外(cause)を必ずつなぐ

try {
    service.process(request);
} catch (java.sql.SQLException e) {
    throw new IllegalStateException("DB処理失敗: reqId=" + request.id(), e);
}
Java

新しい例外へ包むときは、必ず原因(e)をコンストラクタへ渡し、スタックトレースで「どこで・何が」起きたかを辿れるようにします。メッセージには「入力値やID」「期待値」「失敗内容」を含めると、運用で圧倒的に役立ちます。

ログの出し方とレベル

  • 正常系は出さない、失敗だけを適切なレベルで。
  • 一度だけログに出し、再スローしたら上位で重複ログしない(「二重ログ」は原因追跡を難しくします)。
  • スタックトレースは開発・運用ログへ、ユーザー向けには短い説明と問い合わせIDなど。

try-with-resources と外部リソース

自動でクローズして安全に

import java.io.*;
public class Read {
    public static void main(String[] args) throws IOException {
        try (var br = new BufferedReader(new FileReader("input.txt"))) {
            System.out.println(br.readLine());
        } // 例外時でも必ず close
    }
}
Java

ファイル、ソケット、DB接続などは「例外が出ても確実に閉じる」ことが重要。try-with-resources は最短・安全な書き方です。


よくある落とし穴と避け方(重要ポイントの深掘り)

例外の握り潰し

try { dangerous(); } catch (Exception e) { /* 何もしない */ } // NG
Java

無処理は問題の隠蔽です。少なくともログを残し、必要なら再スローまたは既定動作へフォールバックを設計します。

catch の広げ過ぎ

try { ... } catch (Exception e) { ... } // 乱用しない
Java

原因特定が難しくなります。想定する失敗だけを個別に捕まえ、未知の失敗は上位で包括的に処理(「アプリ層での一箇所ハンドリング」)するのが良策です。

例外で制御フローを作らない

「通常の分岐」に例外を使うと可読性と性能が悪化します。バリデーションは前もって実施し、例外は「想定外の異常」に限定します。


カスタム例外と契約

意味のある型を定義する

public class NotEnoughBalanceException extends RuntimeException {
    public NotEnoughBalanceException(String accountId, int needed, int actual) {
        super("残高不足: id=" + accountId + " needed=" + needed + " actual=" + actual);
    }
}
Java

ドメイン固有の失敗は「名前で意味が伝わる型」にします。呼び出し側は型で分岐でき、メッセージは運用へ、型は設計へ効きます。

どこで処理するかを決める

  • インフラ層(DB/IO)で発生 → アプリ層へ包んで再スロー、UI層でユーザーに通知。
  • 入力バリデーションの失敗 → 例外ではなく検証結果として返す(フォームエラーなど)。

例題で身につける

例 1: ファイル読込とエラー報告

import java.nio.file.*;
import java.io.*;
public class ReadFile {
    public static String firstLine(Path path) throws IOException {
        try (var br = Files.newBufferedReader(path)) {
            return br.readLine();
        }
    }
    public static void main(String[] args) {
        try {
            System.out.println(firstLine(Path.of("input.txt")));
        } catch (IOException e) {
            System.err.println("読込失敗: path=" + e.getMessage());
            e.printStackTrace(); // 開発/運用ログへ
        }
    }
}
Java

例 2: 入力検証を前倒しし、例外を減らす

public class Price {
    public static int taxed(int subtotal, double taxRate) {
        if (subtotal < 0) throw new IllegalArgumentException("subtotal must be >= 0");
        if (taxRate < 0 || taxRate > 1) throw new IllegalArgumentException("taxRate must be 0..1");
        return (int) Math.round(subtotal * (1 + taxRate));
    }
}
Java

例 3: カスタム例外で分岐

class Payment {
    static void pay(String accountId, int amount, int balance) {
        if (balance < amount) throw new NotEnoughBalanceException(accountId, amount, balance);
        // 送金処理...
    }
    public static void main(String[] args) {
        try {
            pay("A-001", 1000, 500);
        } catch (NotEnoughBalanceException e) {
            System.err.println("支払い不可: " + e.getMessage());
        }
    }
}
Java

仕上げのアドバイス(重要部分のまとめ)

例外は「失敗の事実と原因を安全に運ぶ」ための道具です。チェック例外は回復可能な外部要因向け、実行時例外は契約違反やバグ向け。try-catch は必要最小限に、後処理は必ず(try-with-resourcesが最良)。原因連鎖で根本原因を残し、握り潰さず、ログは一箇所で責任を持って出す。通常の分岐に例外を使わず、入力検証で前倒しに失敗を表面化する。意味のあるカスタム例外で契約を明確化し、「どこで処理するか」を設計に刻む——この姿勢が、落ちても壊れない堅牢なコードをつくります。

タイトルとURLをコピーしました