例外の全体像
例外(Exception)は「通常の処理の流れを中断して、異常事態を安全に伝える仕組み」です。ファイルがない、ネットワークが切れた、引数が不正、ゼロ除算など「このまま続けると壊れる」状況で、例外を投げて(throw)呼び出し側へ「失敗の事実」と「原因」を届けます。例外を正しく設計・処理できると、プログラムは「失敗しても安全に着地」できるようになります。
仕組みとクラス階層
例外はオブジェクト(Throwable の系譜)
例外はオブジェクトで、Throwable を頂点とするクラス階層に属します。代表は Exception(一般的な例外)と Error(JVMレベルの致命的エラー)。Error はアプリ側で回復しない前提なので、通常は捕捉しません。
Throwable
├─ Error (OutOfMemoryError など/原則扱わない)
└─ Exception
├─ RuntimeException(実行時例外/チェック不要)
└─ チェック例外 (IOException, SQLException など)
例外は「投げる(throw)」「捕まえる(catch)」「伝播させる(throws)」の3動作で扱います。
チェック例外と実行時例外(重要ポイントの深掘り)
2種類の違いと設計指針
- チェック例外(
IOExceptionなど):コンパイラが「処理せよ」と要求。呼び出し側が現実的に回復できる状況向け(再試行、別パス選択など)。 - 実行時例外(
RuntimeException、NullPointerException、IllegalArgumentExceptionなど):プログラムのバグや契約違反。呼び出し側での即時回復は想定せず、入力検証・修正で防ぐべき。
設計の要点は「呼び出し側が合理的に回復できるならチェック例外、そうでなければ実行時例外」。曖昧なら、まずは原因(仕様違反か外部要因か)を言語化してから選びます。
使い方の基本: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) {}
}
Javafinally は「例外の有無に関わらず必ず実行」される後始末用。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が最良)。原因連鎖で根本原因を残し、握り潰さず、ログは一箇所で責任を持って出す。通常の分岐に例外を使わず、入力検証で前倒しに失敗を表面化する。意味のあるカスタム例外で契約を明確化し、「どこで処理するか」を設計に刻む——この姿勢が、落ちても壊れない堅牢なコードをつくります。
