中級 55分 Lesson 6

例外処理

検査/非検査例外・try-catch-finally の落とし穴・finally+return・try-with-resources・multi-catch・カスタム例外・例外チェーンを完全解説

Java Java Silver SE21 例外処理 try-catch try-with-resources

Chapter 06 ─ 例外処理

Silver 試験での出題率は高く、特に「finally の return が try の return を上書き」「try-with-resources のクローズ順序」「multi-catch の制約」「検査/非検査の判定」が頻出。試験コミュニティでも「finally のあとに return を書いたら try の値が消えてびっくりした」という声が多い。


6-1. 例外の階層構造 ─「予期しない事態の分類図」

現実の比喩: 会社の「インシデント管理」に例えてみよう。

事件・事故(Throwable)
├── 天災・戦争(Error)    ─ 会社にはどうにもできない
│    └── 建物崩壊・大停電  ─ 起きたら事業継続を諦めるレベル

└── 業務トラブル(Exception)
     ├── 外部要因(検査例外)   ─ 配送業者が届けられなかった(想定内リスク)
     └── 自社のミス(非検査例外)─ 社員が計算を間違えた(バグ)

Java の全例外は Throwable のサブクラス。大きく ErrorException に分かれる。

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

試験の落とし穴: ErrorThrowable のサブクラスなので技術的には 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 は検査例外、FileNotFoundExceptionIOException のサブクラスなので同じく検査例外。NullPointerExceptionRuntimeException のサブクラスなので非検査。

比較表

検査例外(Checked)非検査例外(Unchecked)
基底クラスException(RuntimeException 除く)RuntimeException, Error
コンパイラ強制throwscatch必須任意
主な原因外部リソース(ファイル・DB・ネット)プログラムのバグ
代表例IOException, SQLExceptionNPE, 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("後片付け");
}

実行フロー:

  1. try を実行
  2. 例外発生 → 対応する catch上から順に 1 つ実行(最初にマッチしたもの、それ以降はスキップ)
  3. 例外なし → catch を全スキップ
  4. finally を実行(2 でも 3 でも)
  5. 捕まえられなかった例外は呼び出し元に伝播

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 が先にあれば、その子クラスである NullPointerExceptionRuntimeException のインスタンスなので先の catch に引っかかる。そうなると後の NullPointerException の catch には絶対に到達できない(到達不能コード)→ コンパイルエラー。

finally の動き ─「絶対に実行される」の意味

finallyreturn, 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"

finallyreturntryreturn を上書きする(試験最頻出)

static int test() {
    try {
        return 1;           // ← これが返るはずだが……
    } finally {
        return 3;           // ← finally の return がすべてを上書き!
    }
}
System.out.println(test()); // → 3(1 は返らない!)

なぜ: tryreturn 1 を実行しようとするが、その前に finally が動く。finally の中に return 3 があるので、そちらが返ってしまう。try の戻り値 1 は消える。

試験口コミ: 「finallyreturntryreturn を消すのを知らなかった」という声が非常に多い。実務では finallyreturn を書くのはバグのもとで避けるべきだが、試験では出題される。

// 例外も同じ: 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 で手動クローズ不要

() の中で宣言した変数は事実上 finaltry ブロック内で再代入不可)。

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. throwthrows ─ スペルが似て非なるもの

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)ecause(原因例外)になる。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

  1. try に入って System.out.print("A ") 実行 → “A ”
  2. return 1 を実行しようとする
  3. その前に finally が動く → System.out.print("C ") → “C ”
  4. finallyreturn 3 が実行 → try の return 1 は破棄
  5. 戻り値は 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: 検査例外かどうかの判定

次の例外クラスを「検査例外」「非検査例外」に分類せよ。

  1. IOException
  2. NullPointerException
  3. FileNotFoundException
  4. ClassCastException
  5. SQLException
  6. StackOverflowError
答え
例外分類理由
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: OKIOExceptionSQLException は親子関係なし
  • B: コンパイルエラーIOExceptionException のサブクラス(親子関係あり)
  • C: コンパイルエラーFileNotFoundExceptionIOException のサブクラス(親子関係あり)

Chapter 06 チェックリスト

  • 例外階層: Throwable → Error / Exception → RuntimeException
  • Error は技術的に catch できるが通常しない(JVM レベルの致命的問題)
  • 検査例外 = Exception のサブクラスで RuntimeException 以外 → throwscatch が必須
  • 非検査例外 = RuntimeException(と Error)のサブクラス → コンパイラ強制なし
  • catch の順序: 子クラスを先(親を先にするとコンパイルエラー)
  • finallyreturn/break/continue/例外スローがあっても必ず実行
  • finallyreturntryreturn を上書きする(試験最頻出)
  • 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) で原因例外を保持する