例外処理
検査/非検査例外・try-catch-finally の落とし穴・finally+return・try-with-resources・multi-catch・カスタム例外・例外チェーンを完全解説
Chapter 06 ─ 例外処理
Silver 試験での出題率は高く、特に「
finallyの return がtryの return を上書き」「try-with-resources のクローズ順序」「multi-catch の制約」「検査/非検査の判定」が頻出。試験コミュニティでも「finally のあとに return を書いたら try の値が消えてびっくりした」という声が多い。
6-1. 例外の階層構造 ─「予期しない事態の分類図」
現実の比喩: 会社の「インシデント管理」に例えてみよう。
事件・事故(Throwable)
├── 天災・戦争(Error) ─ 会社にはどうにもできない
│ └── 建物崩壊・大停電 ─ 起きたら事業継続を諦めるレベル
│
└── 業務トラブル(Exception)
├── 外部要因(検査例外) ─ 配送業者が届けられなかった(想定内リスク)
└── 自社のミス(非検査例外)─ 社員が計算を間違えた(バグ)
Java の全例外は Throwable のサブクラス。大きく Error と Exception に分かれる。
Throwable
├── Error ← JVM・OS レベルの致命的問題(通常 catch しない)
│ ├── OutOfMemoryError ← ヒープメモリ不足
│ ├── StackOverflowError ← 無限再帰でスタック溢れ
│ └── VirtualMachineError ← JVM 内部エラー
│
└── Exception ← プログラムで対処できる問題
├── IOException ← 検査例外(ファイル・ネットワーク)
├── SQLException ← 検査例外(DB アクセス失敗)
├── ClassNotFoundException ← 検査例外(クラスロード失敗)
└── RuntimeException ← 非検査例外(プログラムのバグが多い)
├── NullPointerException ← null 参照のメソッド呼び出し
├── ArrayIndexOutOfBoundsException ← 配列の範囲外アクセス
├── ClassCastException ← 不正なキャスト
├── NumberFormatException ← "abc" を整数に変換しようとした
├── ArithmeticException ← ゼロ除算(整数のみ)
├── IllegalArgumentException ← 引数が不正
├── IllegalStateException ← 不正な状態での呼び出し
└── UnsupportedOperationException ← 未実装操作
Error は「天災」── 通常 catch しない
Error は JVM や OS レベルの問題で、プログラムで回復できないことがほとんど。
// OutOfMemoryError: ヒープが枯渇
int[] huge = new int[Integer.MAX_VALUE]; // OutOfMemoryError
// StackOverflowError: 無限再帰
void infinite() { infinite(); } // StackOverflowError
試験の落とし穴: Error は Throwable のサブクラスなので技術的には catch できる。でも「通常 catch しない」が正しい理解。試験で「Error は catch できない」→ ×。「通常 catch しない(すべき)」→ ○。
Throwable の主要メソッド
try {
int[] arr = new int[3];
arr[5] = 10; // ArrayIndexOutOfBoundsException
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println(e.getMessage()); // "Index 5 out of bounds for length 3"
System.out.println(e.getClass().getName()); // "java.lang.ArrayIndexOutOfBoundsException"
System.out.println(e.toString()); // クラス名 + ": " + getMessage()
e.printStackTrace(); // スタックトレースを標準エラーへ出力
Throwable cause = e.getCause(); // 原因例外(なければ null)
}
| メソッド | 戻り型 | 説明 |
|---|---|---|
getMessage() | String | 例外メッセージ(なければ null) |
toString() | String | クラス名 + ”: ” + getMessage() |
printStackTrace() | void | スタックトレースを標準エラーへ |
getCause() | Throwable | 原因となった例外(例外チェーン用) |
getSuppressed() | Throwable[] | 抑制された例外の配列(try-with-resources 用) |
6-2. 検査例外 vs 非検査例外 ─「コンパイラが強制するかどうか」
非検査例外(Unchecked Exception)
RuntimeException のサブクラス(Error も非検査扱い)。
コンパイラは catch も throws も強制しない。なぜなら「プログラムのバグ(null 参照・配列外アクセス等)は、対処するより根本的に直すべき」という設計思想から。
void badCode(String s) {
s.length(); // NullPointerException の可能性があっても throws 宣言不要
}
// コンパイルは通る。でも s が null なら実行時に NPE で死ぬ。
代表的な非検査例外と発生シナリオ:
// NullPointerException: null 参照に対してメソッド呼び出し
String s = null;
s.length(); // NPE
// ArrayIndexOutOfBoundsException: 配列の範囲外アクセス
int[] arr = new int[3];
arr[5] = 10; // AIOOBE
// ClassCastException: 不正なキャスト
Object obj = "hello";
Integer i = (Integer) obj; // CCE(文字列を Integer にキャストは不可)
// NumberFormatException: 数値以外の文字列を parseInt
int n = Integer.parseInt("abc"); // NFE(NumberFormatException)
// ArithmeticException: 整数のゼロ除算(浮動小数点は Infinity/NaN で例外なし)
int result = 10 / 0; // ArithmeticException: / by zero
// IllegalArgumentException: 不正な引数値
new Thread(null); // IllegalArgumentException(Runnable に null は渡せない)
検査例外(Checked Exception)
Exception のサブクラスで RuntimeException 以外。
コンパイラが「対処したか?」を強制する。対処しないとコンパイルエラー。
void readFile(String path) {
BufferedReader br = new BufferedReader(new FileReader(path)); // コンパイルエラー!
// FileReader のコンストラクタは IOException(検査例外)を throws
// → キャッチするか throws 宣言するかしないとコンパイルできない
}
対処は 2 択:
// 選択肢 1: try-catch で自分でキャッチして対処
void readFile(String path) {
try {
BufferedReader br = new BufferedReader(new FileReader(path));
System.out.println(br.readLine());
} catch (IOException e) {
System.err.println("読み込み失敗: " + e.getMessage());
}
}
// 選択肢 2: throws で呼び出し元に責任を委ねる
void readFile(String path) throws IOException {
BufferedReader br = new BufferedReader(new FileReader(path));
System.out.println(br.readLine());
}
判定フローチャート
例外クラス X を見たとき:
X extends RuntimeException? → はい → 非検査例外(throws/catch 任意)
X extends Exception? → はい → 検査例外(throws か catch が必須)
X extends Error? → はい → 非検査扱い(通常 catch しない)
試験の頻出引っかけ:
IOExceptionは検査例外、FileNotFoundExceptionはIOExceptionのサブクラスなので同じく検査例外。NullPointerExceptionはRuntimeExceptionのサブクラスなので非検査。
比較表
| 検査例外(Checked) | 非検査例外(Unchecked) | |
|---|---|---|
| 基底クラス | Exception(RuntimeException 除く) | RuntimeException, Error |
| コンパイラ強制 | throws か catch が必須 | 任意 |
| 主な原因 | 外部リソース(ファイル・DB・ネット) | プログラムのバグ |
| 代表例 | IOException, SQLException | NPE, ClassCastException |
6-3. try-catch-finally の完全解説
基本の流れ
try {
// ① 保護されたコード(例外が起きるかもしれない処理)
int result = Integer.parseInt(input);
System.out.println("結果: " + result);
} catch (NumberFormatException e) {
// ② ① で NFE が発生したときだけ実行
System.out.println("数値でない入力: " + e.getMessage());
} catch (Exception e) {
// ③ NFE 以外の Exception が発生したときに実行
System.out.println("その他エラー: " + e.getMessage());
} finally {
// ④ 例外の有無にかかわらず「必ず」実行
System.out.println("後片付け");
}
実行フロー:
tryを実行- 例外発生 → 対応する
catchを 上から順に 1 つ実行(最初にマッチしたもの、それ以降はスキップ) - 例外なし →
catchを全スキップ finallyを実行(2 でも 3 でも)- 捕まえられなかった例外は呼び出し元に伝播
catch の順序 ─「子クラスを先に」
// NG: コンパイルエラー!
try {
throw new NullPointerException();
} catch (RuntimeException e) { // RuntimeException は NPE の親
System.out.println("Runtime"); // NPE はここで捕まってしまう
} catch (NullPointerException e) { // ← 到達不能 → コンパイルエラー
System.out.println("NPE");
}
// OK: 子クラスを先に書く
try {
throw new NullPointerException();
} catch (NullPointerException e) { // より具体的(子)を先
System.out.println("NPE");
} catch (RuntimeException e) { // より抽象的(親)を後
System.out.println("その他 Runtime");
}
理由: catch は上から順にマッチを試みる。RuntimeException が先にあれば、その子クラスである NullPointerException も RuntimeException のインスタンスなので先の catch に引っかかる。そうなると後の NullPointerException の catch には絶対に到達できない(到達不能コード)→ コンパイルエラー。
finally の動き ─「絶対に実行される」の意味
finally は return, break, continue, 例外スロー ─ どれがあっても必ず実行される。
// ケース 1: try の return の前に finally が動く
static String test() {
try {
System.out.println("try");
return "from try";
} finally {
System.out.println("finally");
// return を書かない場合、try の "from try" がそのまま返る
}
}
// 出力: "try" → "finally"
// 戻り値: "from try"
finally の return が try の return を上書きする(試験最頻出)
static int test() {
try {
return 1; // ← これが返るはずだが……
} finally {
return 3; // ← finally の return がすべてを上書き!
}
}
System.out.println(test()); // → 3(1 は返らない!)
なぜ: try の return 1 を実行しようとするが、その前に finally が動く。finally の中に return 3 があるので、そちらが返ってしまう。try の戻り値 1 は消える。
試験口コミ: 「
finallyのreturnがtryのreturnを消すのを知らなかった」という声が非常に多い。実務ではfinallyにreturnを書くのはバグのもとで避けるべきだが、試験では出題される。
// 例外も同じ: finally の例外が try の例外を上書き
static void test() {
try {
throw new RuntimeException("try の例外");
} finally {
throw new IllegalStateException("finally の例外"); // こちらが伝播
}
}
// → IllegalStateException が伝播する(RuntimeException は消える)
finally が実行されない唯一のケース: System.exit()
try {
System.exit(0); // JVM 強制終了 → finally は実行されない
} finally {
System.out.println("ここには来ない"); // 実行されない
}
System.exit() は JVM プロセス自体を終了するため、finally も含め以降のコードは一切実行されない。
「JVM プロセスが終了してしまったとき(停電・
kill -9相当)」もfinallyは実行されないが、それは Java コードの範囲外の話。試験で問われるのはSystem.exit()のみ。Thread.sleep()や例外スローはfinallyの実行を妨げない点も注意。
例外の伝播
catch されなかった例外は呼び出しスタックを遡る。
void c() { throw new RuntimeException("C で発生"); } // ←発生
void b() { c(); } // catch しない→伝播
void a() {
try { b(); } // ここで catch
catch (RuntimeException e) {
System.out.println("A でキャッチ: " + e.getMessage());
}
}
// → "A でキャッチ: C で発生"
どこでも catch されなければ、スレッドが終了してスタックトレースが出力される(main スレッドならプログラムが止まる)。
6-4. multi-catch(Java 7+)─ | で複数例外をまとめる
// 従来(冗長、同じ処理が重複)
try { ... }
catch (IOException e) { logger.error(e.getMessage()); }
catch (SQLException e) { logger.error(e.getMessage()); }
// multi-catch(すっきり)
try { ... }
catch (IOException | SQLException e) {
logger.error(e.getMessage());
}
multi-catch の 2 つの制約
制約 1: 変数は事実上 final(再代入不可)
catch (IOException | SQLException e) {
// e = new IOException("別のエラー"); // コンパイルエラー!
// ← 型が 2 種類あり得るので、型が変わる再代入はできない
System.out.println(e.getMessage()); // 読み取りはOK
}
制約 2: 親子関係にある例外を並べてはいけない
catch (Exception | IOException e) { ... }
// コンパイルエラー!
// IOException は Exception のサブクラス → Exception だけ書けば十分 → 冗長で NG
catch (RuntimeException | NullPointerException e) { ... }
// コンパイルエラー!NPE は RuntimeException のサブクラス
理由: 親子関係があると片方が絶対に到達不能になる。コンパイラが「冗長で意味がない」として弾く。
6-5. try-with-resources(Java 7+)─ リソースの自動クローズ
なぜ必要か ─「閉め忘れ」の問題
ファイル・DB コネクション・ソケットは使い終わったら必ず close() しないといけない。でも手動でやると…
BufferedReader br = null;
try {
br = new BufferedReader(new FileReader("file.txt"));
String line = br.readLine();
// ここで例外が起きたら↓のfinally が頼り
} catch (IOException e) {
e.printStackTrace();
} finally {
if (br != null) {
try {
br.close(); // close() 自体も IOException を投げうる
} catch (IOException ignored) {}
}
}
コードが長い・ネストが深い・close 忘れのリスクあり。→ try-with-resources で解決。
AutoCloseable インターフェース
close() メソッドを 1 つだけ持つインターフェース。これを実装したクラスが try-with-resources で使える。
public interface AutoCloseable {
void close() throws Exception;
}
// Closeable は AutoCloseable のサブインターフェース(throws IOException に限定)
public interface Closeable extends AutoCloseable {
void close() throws IOException;
}
// InputStream, OutputStream, Reader, Writer などは Closeable を実装
基本構文
// try の () 内でリソースを宣言・初期化
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
String line = br.readLine();
System.out.println(line);
} catch (IOException e) {
e.printStackTrace();
}
// → try ブロック終了時(例外発生時含む)に br.close() が自動で呼ばれる
// → finally で手動クローズ不要
() の中で宣言した変数は事実上 final(try ブロック内で再代入不可)。
try (BufferedReader br = new BufferedReader(new FileReader("file.txt"))) {
// br = new BufferedReader(...); // コンパイルエラー!再代入不可
br.readLine(); // 読み取りのみOK
}
複数リソースの宣言
try (FileInputStream in = new FileInputStream("in.txt");
FileOutputStream out = new FileOutputStream("out.txt")) {
// 処理
}
// クローズ順序: 宣言の逆順
// → out.close() が先に呼ばれ、次に in.close() が呼ばれる
試験頻出ポイント: 複数リソースのクローズは宣言の逆順。「後に宣言したものが先にクローズされる」。スタック(後入れ先出し)と同じイメージ。
抑制された例外(Suppressed Exception)
try ブロック内で例外が発生し、さらに close() でも例外が発生した場合、どうなるか?
// 従来の finally の場合(bad)
try {
throw new RuntimeException("try 内のエラー");
} finally {
throw new IllegalStateException("close のエラー");
}
// → IllegalStateException が伝播し、RuntimeException は完全に消える(情報損失!)
// try-with-resources の場合(good)
try (MyResource r = new MyResource()) { // close() が例外を投げる
throw new RuntimeException("try 内のエラー");
}
// → RuntimeException が伝播する(主例外)
// → close() の例外は e.getSuppressed() で取り出せる(情報は保持される)
try {
// ...
} catch (RuntimeException e) {
for (Throwable suppressed : e.getSuppressed()) {
System.out.println("抑制された例外: " + suppressed.getMessage());
}
}
口コミ: 「try-with-resources のクローズ順序(逆順)を知らなくて落とした」という声が頻繁に見られる。
6-6. throw と throws ─ スペルが似て非なるもの
throw ─ 例外を実際に投げる文
public void setAge(int age) {
if (age < 0 || age > 150) {
throw new IllegalArgumentException("年齢が不正: " + age);
// ↑ throw はここで実行が止まる(break 的な動き)
}
this.age = age;
}
throw のルール:
Throwableのサブクラスのインスタンスを投げるthrow new 例外クラス()が典型的な形(new忘れはコンパイルエラー)throwは文(statement)なので、後にコードを書いても到達不能になる
void method() {
throw new RuntimeException("エラー");
System.out.println("ここは到達不能"); // コンパイルエラー
}
throws ─ メソッドが投げる可能性のある検査例外を宣言
// readFile は IOException を投げるかもしれないと宣言
public String readFile(String path) throws IOException {
return new BufferedReader(new FileReader(path)).readLine();
}
// 呼び出し側は対処必須
void process() throws IOException { // 自分でも throws するか…
String data = readFile("a.txt");
}
// または
void process2() {
try {
String data = readFile("a.txt");
} catch (IOException e) { ... } // catch するか
}
非検査例外も throws に書ける(コンパイラは強制しないが、JavaDoc 代わりに書くことはある)。
public void validate(String s) throws IllegalArgumentException {
// 非検査例外でも throws に書けるが、コンパイラは強制しない
if (s == null) throw new IllegalArgumentException("null 不可");
}
オーバーライドと throws の制約
class Parent {
void method() throws IOException { }
}
class Child extends Parent {
// OK: IOException のサブクラス FileNotFoundException に絞る(より狭い)
@Override void method() throws FileNotFoundException { }
// OK: throws を完全に省略(例外を投げないと宣言)
// @Override void method() { }
// NG: 親より広い Exception は宣言できない(コンパイルエラー)
// @Override void method() throws Exception { }
// NG: 親が宣言していない別の検査例外(SQL は IO の兄弟クラス)
// @Override void method() throws SQLException { }
}
理由: Parent 型の変数で method() を呼ぶとき、呼び出し側は IOException をキャッチすればよいと信じている。Child が突然 Exception を投げ始めると、その信頼が崩れる。「オーバーライドは親メソッドの throws を広げてはいけない」。
非検査例外は制約外:
class Parent {
void method() { }
}
class Child extends Parent {
@Override void method() throws RuntimeException { } // OK(非検査は自由)
}
6-7. カスタム例外クラス
検査例外として定義(Exception を継承)
public class InsufficientFundsException extends Exception {
private final double shortage;
public InsufficientFundsException(String message, double shortage) {
super(message); // Exception(String) コンストラクタを呼ぶ
this.shortage = shortage;
}
// 例外チェーン対応(原因例外を保持)
public InsufficientFundsException(String message, double shortage, Throwable cause) {
super(message, cause);
this.shortage = shortage;
}
public double getShortage() { return shortage; }
}
// 使い方
public void withdraw(double amount) throws InsufficientFundsException {
if (amount > balance) {
throw new InsufficientFundsException(
"残高不足: 要" + amount + " 残高" + balance,
amount - balance
);
}
balance -= amount;
}
非検査例外として定義(RuntimeException を継承)
public class InvalidOrderStateException extends RuntimeException {
public InvalidOrderStateException(String message) {
super(message);
}
public InvalidOrderStateException(String message, Throwable cause) {
super(message, cause);
}
}
例外チェーン ─ 低レベル例外を高レベルに変換
public User loadUser(int id) throws ServiceException {
try {
return db.findById(id); // SQLException が起きうる
} catch (SQLException e) {
// SQLException のまま上に投げると DB の実装詳細が漏れる
// → アプリ層の例外に変換して、原因は cause として保持
throw new ServiceException("ユーザー取得失敗 id=" + id, e);
}
}
// 呼び出し元
try {
User u = loadUser(42);
} catch (ServiceException e) {
System.out.println(e.getMessage()); // "ユーザー取得失敗 id=42"
System.out.println(e.getCause().getMessage()); // 元の SQLException のメッセージ
}
ポイント: new ServiceException("msg", e) の e が cause(原因例外)になる。new ServiceException("msg") だけだと原因情報が消える。
✏️ 練習問題
問題 1: finally + return の出力
static int method() {
try {
System.out.print("A ");
return 1;
} catch (RuntimeException e) {
System.out.print("B ");
return 2;
} finally {
System.out.print("C ");
return 3;
}
}
public static void main(String[] args) {
System.out.println(method());
}
答え
出力: A C 3
tryに入ってSystem.out.print("A ")実行 → “A ”return 1を実行しようとする- その前に
finallyが動く →System.out.print("C ")→ “C ” finallyのreturn 3が実行 → try のreturn 1は破棄- 戻り値は 3
例外は発生していないので catch の “B ” は出力されない。
問題 2: try-with-resources のクローズ順序
次のカスタムリソースを使ったコードの出力を答えよ。
class Res implements AutoCloseable {
String name;
Res(String name) {
this.name = name;
System.out.println(name + " open");
}
@Override public void close() {
System.out.println(name + " close");
}
}
try (Res a = new Res("A"); Res b = new Res("B")) {
System.out.println("処理");
}
答え
A open
B open
処理
B close
A close
宣言順(A → B)に open され、クローズは逆順(B → A)。
問題 3: 検査例外かどうかの判定
次の例外クラスを「検査例外」「非検査例外」に分類せよ。
IOExceptionNullPointerExceptionFileNotFoundExceptionClassCastExceptionSQLExceptionStackOverflowError
答え
| 例外 | 分類 | 理由 |
|---|---|---|
IOException | 検査例外 | Exception を直接継承(RuntimeException 以外) |
NullPointerException | 非検査例外 | RuntimeException のサブクラス |
FileNotFoundException | 検査例外 | IOException のサブクラス(IOException も検査) |
ClassCastException | 非検査例外 | RuntimeException のサブクラス |
SQLException | 検査例外 | Exception を継承(RuntimeException 以外) |
StackOverflowError | (非検査扱い) | Error のサブクラス(通常 catch しない) |
問題 4: multi-catch の適法性
// A
catch (IOException | SQLException e) { ... }
// B
catch (Exception | IOException e) { ... }
// C
catch (FileNotFoundException | IOException e) { ... }
答え
- A: OK ─
IOExceptionとSQLExceptionは親子関係なし - B: コンパイルエラー ─
IOExceptionはExceptionのサブクラス(親子関係あり) - C: コンパイルエラー ─
FileNotFoundExceptionはIOExceptionのサブクラス(親子関係あり)
Chapter 06 チェックリスト
- 例外階層:
Throwable → Error / Exception → RuntimeException -
Errorは技術的に catch できるが通常しない(JVM レベルの致命的問題) - 検査例外 =
ExceptionのサブクラスでRuntimeException以外 →throwsかcatchが必須 - 非検査例外 =
RuntimeException(とError)のサブクラス → コンパイラ強制なし - catch の順序: 子クラスを先(親を先にするとコンパイルエラー)
-
finallyはreturn/break/continue/例外スローがあっても必ず実行 -
finallyのreturnはtryのreturnを上書きする(試験最頻出) -
finallyが実行されない唯一のケース:System.exit()の呼び出し -
finally内の例外もtry内の例外を上書き(情報消失) - multi-catch の変数は事実上
final(再代入不可) - multi-catch で親子関係にある例外は並べられない(コンパイルエラー)
- try-with-resources:
AutoCloseableを実装したリソースが自動でクローズ - リソースの変数は事実上
final(try ブロック内での再代入不可) - 複数リソースのクローズ順: 宣言の逆順
- try-with-resources での close 例外は
getSuppressed()で取り出せる(情報保持) -
throw new 例外クラス()で例外を投げる(newが必要) -
throwsはメソッドシグネチャに書く検査例外の宣言 - オーバーライド時の
throws: 親より広い検査例外は不可、非検査例外は自由 - カスタム検査例外は
Exception継承、非検査例外はRuntimeException継承 - 例外チェーン:
new MyException("msg", cause)で原因例外を保持する